feat(security): enforce strict host validation and authorized origin checks on ccsp routes
This commit is contained in:
+38
@@ -1046,7 +1046,45 @@ public class CcspServer {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean validateHost(HttpExchange exchange) throws IOException {
|
||||||
|
String hostHeader = exchange.getRequestHeaders().getFirst("Host");
|
||||||
|
if (hostHeader == null || hostHeader.isBlank()) {
|
||||||
|
logger.warn("Requisição CCSP rejeitada: cabeçalho 'Host' ausente.");
|
||||||
|
sendErrorResponse(exchange, 403, "INVALID_HOST", "Cabeçalho Host é obrigatório.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String rawHost = hostHeader.trim();
|
||||||
|
String host;
|
||||||
|
if (rawHost.startsWith("[")) {
|
||||||
|
int closingBracket = rawHost.indexOf(']');
|
||||||
|
if (closingBracket != -1) {
|
||||||
|
host = rawHost.substring(0, closingBracket + 1);
|
||||||
|
} else {
|
||||||
|
host = rawHost;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int colonIdx = rawHost.indexOf(':');
|
||||||
|
host = colonIdx != -1 ? rawHost.substring(0, colonIdx) : rawHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isAllowedHost = "127.0.0.1".equalsIgnoreCase(host)
|
||||||
|
|| "localhost".equalsIgnoreCase(host)
|
||||||
|
|| "[::1]".equalsIgnoreCase(host);
|
||||||
|
|
||||||
|
if (!isAllowedHost) {
|
||||||
|
logger.warn("ALERTA DE SEGURANÇA: Requisição CCSP rejeitada por suspeita de DNS Rebinding. Host: '{}'", hostHeader);
|
||||||
|
sendErrorResponse(exchange, 403, CcspErrorCode.INVALID_ORIGIN.name(), "Host não autorizado para loopback local.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean handleCors(HttpExchange exchange) throws IOException {
|
private boolean handleCors(HttpExchange exchange) throws IOException {
|
||||||
|
if (!validateHost(exchange)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
String path = exchange.getRequestURI().getPath();
|
String path = exchange.getRequestURI().getPath();
|
||||||
String origin = getOrigin(exchange);
|
String origin = getOrigin(exchange);
|
||||||
|
|
||||||
|
|||||||
+38
@@ -615,4 +615,42 @@ class CcspServerTest {
|
|||||||
assertThat(resp.body()).contains("\"signed_xml\":");
|
assertThat(resp.body()).contains("\"signed_xml\":");
|
||||||
assertThat(resp.body()).contains("QualifyingProperties");
|
assertThat(resp.body()).contains("QualifyingProperties");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Requisição com cabeçalho Host não-loopback (DNS Rebinding) deve ser rejeitada com 403")
|
||||||
|
void testDnsRebindingHostRejection() throws Exception {
|
||||||
|
int port = server.getPort();
|
||||||
|
try (java.net.Socket socket = new java.net.Socket("127.0.0.1", port);
|
||||||
|
java.io.OutputStream out = socket.getOutputStream();
|
||||||
|
java.io.BufferedReader in = new java.io.BufferedReader(new java.io.InputStreamReader(socket.getInputStream()))) {
|
||||||
|
|
||||||
|
String rawRequest = "GET /ccsp/v1/status HTTP/1.1\r\n" +
|
||||||
|
"Host: evil.attacker.com\r\n" +
|
||||||
|
"Connection: close\r\n\r\n";
|
||||||
|
out.write(rawRequest.getBytes(StandardCharsets.UTF_8));
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
String statusLine = in.readLine();
|
||||||
|
assertThat(statusLine).contains("403");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Requisição com cabeçalho Host loopback válido deve ser aceita")
|
||||||
|
void testValidLoopbackHostAcceptance() throws Exception {
|
||||||
|
int port = server.getPort();
|
||||||
|
try (java.net.Socket socket = new java.net.Socket("127.0.0.1", port);
|
||||||
|
java.io.OutputStream out = socket.getOutputStream();
|
||||||
|
java.io.BufferedReader in = new java.io.BufferedReader(new java.io.InputStreamReader(socket.getInputStream()))) {
|
||||||
|
|
||||||
|
String rawRequest = "GET /ccsp/v1/status HTTP/1.1\r\n" +
|
||||||
|
"Host: 127.0.0.1:" + port + "\r\n" +
|
||||||
|
"Connection: close\r\n\r\n";
|
||||||
|
out.write(rawRequest.getBytes(StandardCharsets.UTF_8));
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
String statusLine = in.readLine();
|
||||||
|
assertThat(statusLine).contains("200");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user