Este documento descreve as práticas de segurança recomendadas ao usar o @vanaware/wsrouter em produção.
- HTTPS e HSTS
- Proteção de Arquivos Estáticos
- WebSocket Security
- Autenticação e Autorização
- Rate Limiting
- Headers de Segurança
O router suporta redirecionamento automático de HTTP para HTTPS:
const app = createDenoRouter({
basePath: "/api",
forceHttps: true, // Redireciona HTTP → HTTPS
});Comportamento:
- Redireciona com status
301 Moved Permanently - Ignora automaticamente
localhoste127.0.0.1para facilitar desenvolvimento - Suporta IPv6
[::1]
Quando atrás de um proxy reverso (nginx, Cloudflare, etc.), o router pode confiar no header X-Forwarded-Proto:
const app = createDenoRouter({
forceHttps: true,
trustProxy: true, // ⚠️ Apenas se estiver atrás de proxy confiável
});trustProxy se o servidor estiver exposto diretamente à internet. Um atacante poderia enviar X-Forwarded-Proto: https e bypassar o redirect.
Quando forceHttps está ativo e a requisição já é HTTPS, o router automaticamente adiciona:
Strict-Transport-Security: max-age=31536000; includeSubDomainsIsso instrui navegadores a sempre usar HTTPS para seu domínio.
Por padrão, arquivos que começam com . são bloqueados:
const app = createDenoRouter({
staticDir: "./public",
allowDotfiles: false, // Default: false
});Bloqueados por padrão:
.env,.env.local.git/config,.git/HEAD.DS_Store.htaccess
Mesmo quando allowDotfiles: true é habilitado (por exemplo, para servir .well-known), segmentos de diretório relativo como .. e . continuam estritamente proibidos e bloqueados com 404, garantindo que a ativação de dotfiles nunca abra brechas de path traversal.
O adaptador Deno recusa symlinks diretamente e valida a resolução física com Deno.realPath:
- Symlinks diretos são detectados via
Deno.lstate recusados. - Diretórios intermediários symlink são validados contra
resolvedBase, impedindo que symlinks de diretório apontando para fora dostaticDirsejam acessados.
O adaptador Deno injeta automaticamente:
X-Content-Type-Options: nosniff(prevenindo ataques de sniffing MIME)ETage suporte transparente aIf-None-Match(retornando304 Not Modifiede cancelando o stream para economia de I/O)Cache-ControleLast-Modified- Tratamento seguro de requisições
HEAD, cancelando o stream aberto para evitar vazamento de file descriptors.
O router sanitiza caminhos para evitar ataques de path traversal:
// Requisição maliciosa
GET /../../etc/passwd
GET /..%2F..%2Fetc%2Fpasswd
// Resultado: 404 (caminho sanitizado antes de acessar)O adaptador Deno verifica que o caminho resolvido está estritamente dentro do staticDir:
const app = createDenoRouter({
staticDir: "/var/www/public",
});
// Requisição: GET /../../etc/passwd
// Mesmo após sanitização, o path resolvido é verificado
// Resultado: 404 se tentar escapar do diretórioWebSockets são vulneráveis a ataques Cross-Site WebSocket Hijacking (CSWSH). Valide o header Origin:
const allowedOrigins = ["https://meusite.com", "https://app.meusite.com"];
app.use(async (req, _params, next) => {
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
const origin = req.headers.get("origin");
if (!origin || !allowedOrigins.includes(origin)) {
return new Response("Forbidden: Invalid origin", { status: 403 });
}
}
return await next();
});O exemplo JWT demonstra como passar tokens via subprotocolo WebSocket:
// Cliente
const ws = new WebSocket("wss://api.site.com/chat", ["Bearer", token]);
// Servidor (middleware)
app.use(async (req, _params, next) => {
if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") {
return await next();
}
const protocol = req.headers.get("sec-websocket-protocol") ?? "";
const protocols = protocol.split(",").map(p => p.trim());
const bearerIndex = protocols.findIndex(p => p === "Bearer");
const token = bearerIndex !== -1 ? protocols[bearerIndex + 1] : null;
if (!token) {
return new Response("Token required", { status: 401 });
}
try {
await jwtVerify(token, secret);
return await next();
} catch {
return new Response("Invalid token", { status: 403 });
}
});Use permissionFn para garantir que mensagens não vazem entre salas:
app.ws("/chat/:room/:user", (ws, _req, params) => {
const group = app.getWsGroupByPath("/chat/:room/:user");
ws.onmessage = (event) => {
group.broadcast(
`[${params.user}]: ${event.data}`,
(receiver, sender, _msg) => receiver.room === sender.room,
params
);
};
});Sem permissionFn, o broadcast envia para TODOS os sockets da rota, não apenas para a mesma sala.
Sempre defina expiração em tokens JWT:
import { SignJWT } from "jose";
const token = await new SignJWT({ userId: "123", role: "user" })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("1h") // ⚠️ Sempre definir
.setIssuedAt()
.sign(secret);Ao verificar JWTs, especifique algoritmos permitidos:
import { jwtVerify } from "jose";
await jwtVerify(token, secret, {
algorithms: ["HS256"], // ⚠️ Evite "none" e algoritmos fracos
});Nunca hardcode segredos em produção:
// ❌ RUIM
const JWT_SECRET = "meu-segredo-123";
// ✅ BOM
const JWT_SECRET = Deno.env.get("JWT_SECRET");
if (!JWT_SECRET) {
throw new Error("JWT_SECRET não configurado");
}O router não inclui rate limiting nativo. Use middlewares para proteger contra abuso:
// Exemplo simples de rate limiting por IP
const requestCounts = new Map<string, { count: number; resetTime: number }>();
app.use(async (req, _params, next) => {
const ip = req.headers.get("x-forwarded-for")?.split(",")[0] ?? "unknown";
const now = Date.now();
const windowMs = 60000; // 1 minuto
const maxRequests = 100;
const record = requestCounts.get(ip) ?? { count: 0, resetTime: now + windowMs };
if (now > record.resetTime) {
record.count = 0;
record.resetTime = now + windowMs;
}
record.count++;
requestCounts.set(ip, record);
if (record.count > maxRequests) {
return new Response("Too Many Requests", {
status: 429,
headers: {
"Retry-After": Math.ceil((record.resetTime - now) / 1000).toString(),
},
});
}
return await next();
});Para produção, considere:
- Redis para rate limiting distribuído
- Bibliotecas especializadas como
rate-limiter-flexible - Soluções de edge (Cloudflare, AWS WAF)
Adicione headers de segurança via middleware:
app.use(async (req, _params, next) => {
const res = await next();
// Prevenir clickjacking
res.headers.set("X-Frame-Options", "DENY");
// Prevenir MIME sniffing
res.headers.set("X-Content-Type-Options", "nosniff");
// Referrer Policy
res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
// Content Security Policy (ajuste conforme necessário)
res.headers.set(
"Content-Security-Policy",
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
);
return res;
});-
forceHttps: trueativado -
trustProxy: trueapenas se atrás de proxy confiável -
allowDotfiles: false(default) - Validação de
Originem WebSockets - Tokens JWT com expiração e algoritmos restritos
- Segredos em variáveis de ambiente
- Rate limiting implementado
- Headers de segurança adicionados
- Logs de auditoria para autenticação
- HTTPS/WSS em produção (nunca HTTP/WS)