feat(security): enforce strict host validation and authorized origin checks on ccsp routes

This commit is contained in:
Claudecio Martins
2026-10-20 14:53:10 -03:00
parent eeeecb0604
commit 1a727e6ff5
2 changed files with 76 additions and 0 deletions
@@ -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);
@@ -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");
}
}
} }