1. /api/render: cap de 512 KiB na entrada (amplificação ~2× medida); decodeJSON agora amarra http.MaxBytesReader ao ResponseWriter (413 real ao cliente em todos os endpoints JSON) 2. Login com rate-limit por IP em memória (5 falhas/15 min → 429, acerto reseta, RemoteAddr puro — X-F-F não é confiável) + log de falha com IP 3. Upload: MaxBytesReader no corpo inteiro (64 MiB + 64 KiB) antes do parse do multipart; quota global de 2 GiB (MaxFilesTotalBytes) em AddFile 4. sanitizeRich neutraliza href/src com javascript:/vbscript:/data: (case/whitespace-insensível); link legítimo preservado 5. Cookie Secure passa a ser configuração explícita (NOTES_SECURE_COOKIE) — X-Forwarded-Proto é spoofável; documentado em .env.example e compose 6. Cookie agora carrega id de sessão opaco (crypto/rand 32B, TTL 30d, revogação no logout e no restart); token mestre só vale via Bearer — furo pego pelo próprio teste novo (token no cookie era aceito) Testes: rate-limit, revogação, canais separados, render 413, upload cap sw v16
80 lines
2.3 KiB
JavaScript
80 lines
2.3 KiB
JavaScript
/* Service Worker do Notas — caches versionados.
|
|
* BUMP de CACHE em todo deploy que mude shell/assets (o activate limpa os antigos).
|
|
*/
|
|
const CACHE = 'notas-shell-v16';
|
|
const SHELL = [
|
|
'/',
|
|
'/css/app.css',
|
|
'/js/app.js',
|
|
'/manifest.webmanifest',
|
|
'/icons/icon-192.png',
|
|
'/icons/icon-512.png',
|
|
'/icons/maskable-192.png',
|
|
'/icons/maskable-512.png',
|
|
'/icons/monochrome-512.png',
|
|
];
|
|
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
caches.open(CACHE)
|
|
.then((cache) => cache.addAll(SHELL))
|
|
.then(() => self.skipWaiting()),
|
|
);
|
|
});
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches.keys()
|
|
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
|
|
.then(() => self.clients.claim())
|
|
.then(() => self.clients.matchAll({ type: 'window' }))
|
|
.then((clients) => {
|
|
for (const client of clients) client.postMessage('notas:updated');
|
|
}),
|
|
);
|
|
});
|
|
|
|
self.addEventListener('message', (event) => {
|
|
if (event.data === 'notas:skip-waiting') self.skipWaiting();
|
|
});
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const req = event.request;
|
|
const url = new URL(req.url);
|
|
|
|
// 1. API e SSE: NUNCA interceptar/cache — dados devem ser sempre frescos
|
|
// (interceptar SSE quebraria o stream de tempo real).
|
|
if (url.origin !== self.location.origin || url.pathname.startsWith('/api/')) return;
|
|
|
|
// 2. Navegação (abrir o app): network-first com fallback ao shell em cache (offline).
|
|
if (req.mode === 'navigate' || req.destination === 'document') {
|
|
event.respondWith(
|
|
fetch(req)
|
|
.then((resp) => {
|
|
const copy = resp.clone();
|
|
caches.open(CACHE).then((c) => c.put('/', copy));
|
|
return resp;
|
|
})
|
|
.catch(() => caches.match('/')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// 3. Estáticos (css/js/ícones/fontes): stale-while-revalidate —
|
|
// mostra o cache na hora e atualiza em background.
|
|
event.respondWith(
|
|
caches.match(req).then((hit) => {
|
|
const refresh = fetch(req)
|
|
.then((resp) => {
|
|
if (resp && resp.ok) {
|
|
const copy = resp.clone();
|
|
caches.open(CACHE).then((c) => c.put(req, copy));
|
|
}
|
|
return resp;
|
|
})
|
|
.catch(() => hit);
|
|
return hit || refresh;
|
|
}),
|
|
);
|
|
});
|