Close the residual gaps the 2026-09-18 audit left open
Re-checked every item of the audit's own outcome section against the tree.
Most held, but five items marked fixed had residual gaps — one of them a
live IDOR — and the deferred list was untouched.
Security:
- Analytics IDOR: /api/analytics/{ws}/overview and /projection took
child_id as an unchecked query param, and get_overview called
get_days_left_at_age_4 without workspace_id, so the ownership guard
never ran. Any member could read another workspace's child projection.
Fixed at both layers.
- Scraper: session ids no longer logged at INFO; a persist-callback
failure's raw exception text and the FK maintenance banner's scraped
text no longer reach the poll client.
- Email is now case-normalised at the API boundary, with migration 009
lowercasing existing rows (aborts naming colliding rows rather than
failing opaquely on the UNIQUE constraint).
Frontend correctness:
- usePlanProposal/usePlanSync gained the stale-response guard the other
hooks already had.
- PlanSyncProvider no longer fires POST /plan/sync before auth resolves
off a blindly-read localStorage workspaceId.
- Busy flags are cleared unconditionally across all 16 useLatestRequest
consumers. setBusy(true) was unconditional while the clear was guarded
by isCurrent, so a superseded call stranded its flag forever — under
StrictMode this hung every in-app navigation behind the PageSkeleton.
isCurrent decides which response's data wins, never whether a call
releases the flag it raised.
Performance:
- Calendar no longer blanks on a mutation, and a multi-day save issues N
writes plus one background refetch instead of 2N requests.
- take_pl_on_red_days moved onto GET /auth/context, dropping a redundant
GET /workspace/:id from every cold load.
CI and repo hygiene: least-privilege permissions block, actions pinned to
commit SHAs, npm ci, uv sync --locked, failure-only artifact upload,
dependabot; dead frontend/out.js removed; CSP added to the nginx template
(repeated into the location blocks that set their own add_header, since
nginx does not merge them across levels).
Left deferred by decision: refresh-token revocation, the phase-2 contract
change, M4, M5, L2, and H12's cross-route cache. Details and the smaller
follow-ups browser verification turned up are in the audit report.
Backend 901 passed, coverage 92%. Frontend 1322 tests, coverage
98.17/94.79/92.14/98.17.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PngVF38MLpdkgXcByKfdW6
This commit is contained in:
@@ -22,10 +22,23 @@ server {
|
||||
|
||||
server_tokens off;
|
||||
|
||||
# nginx does NOT merge add_header across levels: if a location block adds
|
||||
# even one add_header of its own, every add_header set here at server
|
||||
# level is discarded for responses from that location — not appended to.
|
||||
# /assets/, /fonts/ and = /index.html below each set their own
|
||||
# Cache-Control, so the four security headers are repeated verbatim
|
||||
# inside each of those blocks. Add a header here, and it must be added
|
||||
# there too (or it silently never reaches those responses).
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-Frame-Options DENY always;
|
||||
add_header Referrer-Policy no-referrer always;
|
||||
|
||||
# Derived from the actual production build (frontend/dist): no inline
|
||||
# <script>/<style>, all JS/CSS/fonts same-origin, the only data: URIs are
|
||||
# an <img> (QR code) and a CSS background-image (chevron icon) — hence
|
||||
# img-src needs data: but nothing else does. API is same-origin /api/.
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
|
||||
|
||||
# Container-level liveness. Served from memory, so a broken bundle or a
|
||||
# read-only mount cannot make this lie.
|
||||
location = /healthz {
|
||||
@@ -58,6 +71,12 @@ server {
|
||||
# `expires 1y` would emit its own Cache-Control alongside this one;
|
||||
# one explicit header is clearer than two contradicting-looking ones.
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
# Repeated: see the server-level comment above — an add_header here
|
||||
# discards the server block's add_headers for this location.
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-Frame-Options DENY always;
|
||||
add_header Referrer-Policy no-referrer always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
@@ -66,6 +85,11 @@ server {
|
||||
location /fonts/ {
|
||||
access_log off;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
# Repeated: see the server-level comment above.
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-Frame-Options DENY always;
|
||||
add_header Referrer-Policy no-referrer always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
@@ -73,6 +97,13 @@ server {
|
||||
# leaves clients pinned to a stale asset graph.
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache" always;
|
||||
# Repeated: see the server-level comment above — this is the response
|
||||
# the CSP/X-Frame-Options/etc. exist to protect, so it must not lose
|
||||
# them just because this block also sets Cache-Control.
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-Frame-Options DENY always;
|
||||
add_header Referrer-Policy no-referrer always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
|
||||
}
|
||||
|
||||
# SPA fallback. react-router-dom uses BrowserRouter, so a hard refresh or a
|
||||
|
||||
Reference in New Issue
Block a user