Segurança
Ligado por padrão:
| Proteção | Detalhe |
|---|---|
| Headers de segurança | X-Content-Type-Options: nosniff, Content-Security-Policy: default-src 'none'; frame-ancestors 'none', Referrer-Policy: no-referrer, X-Frame-Options: DENY, Cross-Origin-Resource-Policy: same-origin. HSTS é opt-in. Para servir páginas, use securityHeaders: 'web' (CSP default-src 'self', X-Frame-Options: SAMEORIGIN). |
| Limite de body | 1 MiB (bodyLimit). Recusado já pelo Content-Length, com a conexão fechada. |
| Timeouts | requestTimeout 30s, headersTimeout 10s, keepAliveTimeout 5s (contra Slowloris). |
| Query | Até 1000 pares (queryMaxKeys); __proto__, constructor e prototype são descartados. |
| Body JSON | __proto__ e constructor.prototype em qualquer nível, inclusive com escapes \u, recebem 400 (PROTO_POISONING). Com protoPoisoning: 'remove', as chaves são descartadas e a requisição segue. |
| Logs | authorization, cookie, password, token, apiKey e afins saem como [REDACTED] no logger padrão. |
| Erros 5xx | Sem detalhes para o cliente. |
| Request id | Ids recebidos só são aceitos se forem seguros (contra log injection). |
| Host e path | Host malformado → 400. O path vem do request-target com base fixa: //evil.com/x continua sendo esse path e não muda o host. ctx.url usa o host validado. |
Opt-in:
import { cors, createApp, csrf, rateLimit } from 'flowingts';
createApp({
requireSchemas: true,
securityHeaders: { strictTransportSecurity: true }, // só atrás de HTTPS
})
.use(cors({ origin: ['https://app.exemplo.com', 'https://*.exemplo.dev'], credentials: true }))
.use(rateLimit({ max: 100, windowMs: 60_000 })); // 429 + Retry-Aftercors(): a origem é obrigatória, sem default permissivo. Responde ao preflight com 204 e enviaVary: Origin. Combinarorigin: '*'comcredentials: truegera erro.Regras de origem e de host, seguras por padrão (as mesmas para
cors()eallowedHosts):Regra Casa com Não casa com 'https://app.exemplo.com'a mesma origem, depois de normalizar (maiúsculas, porta padrão, barra final) http://…, outra porta,app.exemplo.com.atacante.com'https://*.exemplo.dev'qualquer subdomínio exemplo.devem si,exemplo.dev.atacante.comRegExp a string inteira, como se tivesse ^…$qualquer casamento parcial função o que ela decidir, sob sua responsabilidade — - RegExp sem âncora não vira brecha:
/app\.exemplo\.com/não aceitaapp.exemplo.com.atacante.com. As flagsg,yemsão descartadas, então o resultado não muda entre requisições. - Prefira o curinga à RegExp: uma regex mal escrita pode ser lenta com uma entrada maliciosa (ReDoS).
- Regras inválidas falham na inicialização, por exemplo uma origem sem esquema (
'app.exemplo.com').
- RegExp sem âncora não vira brecha:
Autenticação:
bearerAuth()ebasicAuth(). São guards prontos. A verificação é sua: JWT com a biblioteca que preferir, sessão no banco, lista de chaves. O usuário devolvido vai paractx.state.user.tsconst auth = bearerAuth({ verify: async (token) => (await sessions.find(token)) ?? false }); app.group({ guards: [auth] }, (g) => g.mount(UserRoutes)); const painel = basicAuth({ verify: (user, pass) => safeEqual(user, 'admin') && safeEqual(pass, process.env.ADMIN_PASS!) });Credencial ausente, malformada ou recusada recebe o mesmo 401, com
WWW-Authenticatee sem dizer o motivo. Para comparar segredos, usesafeEqual(), que tem tempo constante e também não revela o tamanho. Use obasicAuthsó sobre HTTPS.rateLimit(): guarda os contadores em memória por padrão. Para várias instâncias, implementeRateLimitStore(Redis etc.).Cookies:
ctx.setCookie()usaHttpOnly,Secure,SameSite=LaxePath=/por padrão (os navegadores aceitamSecureemhttp://localhost). Nome,pathedomainsão validados contra injeção de atributos, e os prefixos__Host-e__Secure-têm as regras conferidas.csrf(): ative sempre que a autenticação usar cookies. Métodos que alteram estado (todos menos GET, HEAD e OPTIONS) vindos de outra origem recebem 403. A checagem usa oSec-Fetch-Sitee, na falta dele, compara oOrigincom a própria origem. Requisições sem esses headers (curl, servidor a servidor) passam, porque não há navegador para anexar os cookies da vítima.tsapp.use(csrf()); // checagem de origem app.use(csrf({ trustedOrigins: ['https://app.exemplo.com'] })); // front em outra origem app.use(csrf({ token: true })); // + double-submit tokenCom
token: true, o middleware cria o cookie__Host-csrf(legível pelo JS,SameSite=Strict) e o expõe emctx.state.csrfToken; o cliente o devolve no headerX-CSRF-Token. Rotas GET nunca devem alterar estado.Atrás de proxy:
ctx.ip,ctx.hostectx.protocolusam o socket e oHostpor padrão. ComtrustProxy: 1(o número de proxies à frente do app) outrue(toda a cadeia), passam a usar oX-Forwarded-For, o-Hoste o-Proto. Só ligue isso atrás de um proxy que reescreve esses headers, porque o cliente pode forjá-los.allowedHosts: comallowedHosts: ['api.exemplo.com', '*.exemplo.dev'], qualquer outroHostrecebe 400. Protege contra host header injection e DNS rebinding. Um host sem porta aceita qualquer porta; com porta ('api.exemplo.com:8443'), exige a porta exata.Links absolutos (reset de senha, redirects): prefira uma URL base da configuração em vez do host pedido pelo cliente.