From 1a727e6ff597d3306c7344a7464a70a3df54ec18 Mon Sep 17 00:00:00 2001 From: Claudecio Martins Date: Tue, 20 Oct 2026 14:53:10 -0300 Subject: [PATCH] feat(security): enforce strict host validation and authorized origin checks on ccsp routes --- .../signer/core/protocol/CcspServer.java | 38 +++++++++++++++++++ .../signer/core/protocol/CcspServerTest.java | 38 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java index c0ed29b..5ab163c 100644 --- a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java @@ -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); diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspServerTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspServerTest.java index f922f52..280d87c 100644 --- a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspServerTest.java +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspServerTest.java @@ -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"); + } + } }