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;
|
||||
}
|
||||
|
||||
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 {
|
||||
if (!validateHost(exchange)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String path = exchange.getRequestURI().getPath();
|
||||
String origin = getOrigin(exchange);
|
||||
|
||||
|
||||
+38
@@ -615,4 +615,42 @@ class CcspServerTest {
|
||||
assertThat(resp.body()).contains("\"signed_xml\":");
|
||||
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