Cache and log hardening - phase 0 of the #189 data-minimization track #313

Merged
robbertbos merged 1 commit from phase-0-leak-hygiene into main 2026-08-15 20:39:55 +00:00
Owner

Research for #189 identified twelve cache/log leak surfaces that are independent of any architecture choice. This PR closes them (phase 0); the direction itself (refs-only cards, content on demand) stays in #189.

At the transport edge:

  • Every /api response defaults to Cache-Control: no-store; the byte-proxy caches only inline raster images, for an hour; avatars and emoji keep their cache.
  • Typed text no longer travels in request lines: mention-candidates (card + channel), resolve-mentions and people search are POST with the text in the body (resolve-mentions and people now take real lists). Accepted exception: the PDF preview keeps the real filename as its path segment, because the browser viewer titles itself after it (see security.md).
  • The production image runs uvicorn with --no-access-log; the ZAD ingress keeps the single copy of request lines. The pod writes its own scrubbed request log instead (status, duration, pseudonymous user/session ids; no IPs or query strings), and oidc_sessions.last_seen_at marks session liveness once a minute.

In the application:

  • SQLAlchemy errors stringify without bind parameters (hide_parameters), so a failed card insert cannot put message text in a traceback.
  • 422 responses carry type/loc/msg but never the submitted input; import validation details name field paths, not values.
  • The rate limiter refusal log carries ip: instead of a raw client IP (same treatment as audit_events).
  • sync_logs rows age out after 90 days (WAGGLE_SYNC_LOG_RETENTION_DAYS), pruned on the audit-purge tick.

In the browser:

  • Logout sends Clear-Site-Data: "cache", "storage" (theme rehydrates from the settings table) and sweeps localStorage traces; the 401 path sweeps too.

Docs: security.md gains a section on response caching, browser storage, access logs and the shared-workstation scenario; deployment.md gains WAGGLE_LOG_LEVEL (never DEBUG in production) and WAGGLE_SYNC_LOG_RETENTION_DAYS.

Gates: 1970 backend tests at 100% coverage, 1177 vitest, vue-tsc + build, pinned pre-commit across all files, full e2e suite locally green after rebase.

Research for #189 identified twelve cache/log leak surfaces that are independent of any architecture choice. This PR closes them (phase 0); the direction itself (refs-only cards, content on demand) stays in #189. At the transport edge: - Every /api response defaults to Cache-Control: no-store; the byte-proxy caches only inline raster images, for an hour; avatars and emoji keep their cache. - Typed text no longer travels in request lines: mention-candidates (card + channel), resolve-mentions and people search are POST with the text in the body (resolve-mentions and people now take real lists). Accepted exception: the PDF preview keeps the real filename as its path segment, because the browser viewer titles itself after it (see security.md). - The production image runs uvicorn with --no-access-log; the ZAD ingress keeps the single copy of request lines. The pod writes its own scrubbed request log instead (status, duration, pseudonymous user/session ids; no IPs or query strings), and oidc_sessions.last_seen_at marks session liveness once a minute. In the application: - SQLAlchemy errors stringify without bind parameters (hide_parameters), so a failed card insert cannot put message text in a traceback. - 422 responses carry type/loc/msg but never the submitted input; import validation details name field paths, not values. - The rate limiter refusal log carries ip:<hash> instead of a raw client IP (same treatment as audit_events). - sync_logs rows age out after 90 days (WAGGLE_SYNC_LOG_RETENTION_DAYS), pruned on the audit-purge tick. In the browser: - Logout sends Clear-Site-Data: "cache", "storage" (theme rehydrates from the settings table) and sweeps localStorage traces; the 401 path sweeps too. Docs: security.md gains a section on response caching, browser storage, access logs and the shared-workstation scenario; deployment.md gains WAGGLE_LOG_LEVEL (never DEBUG in production) and WAGGLE_SYNC_LOG_RETENTION_DAYS. Gates: 1970 backend tests at 100% coverage, 1177 vitest, vue-tsc + build, pinned pre-commit across all files, full e2e suite locally green after rebase.
Authenticated JSON (card snapshots, threads, drafts) was served without any
cache directive, so browsers could write it to the disk cache and replay it
on back/forward navigation, surviving logout. The middleware now sets
no-store as a default; routes that cache deliberately keep their own header
via setdefault.
The file proxy sent private, max-age=86400 for every MIME type, parking PDF,
text, video and forced downloads in the browser's disk cache for a day,
beyond logout. Only inline png/jpeg/gif/webp keep the day cache; everything
else, plus ?disposition=attachment and ?preview=html, is no-store now.
_AVATAR_MIMES is renamed _RASTER_IMAGE_MIMES because the same set now backs
the cache rule.
Pydantic puts the offending input in every 422 error dict and in
str(ValidationError); on the import route that reflects a whole export
bundle (card text, notes) back in the response. 422 errors now carry only
type/loc/msg, and the import problem details name field paths instead of
values.
StatementError.__str__ appends [parameters: ...] and several background
loops log str(exc); a failed card or draft insert therefore wrote the full
message snapshot into the log. hide_parameters=True removes the values at
the source.
slowapi logs the bucket key on every refusal, which put the raw client IP
in the application log while audit_events deliberately stores only a hash.
The IP fallback now hashes the address with the same helper; per-user keys
are unchanged and bucketing behaviour is identical.
Attachment viewing is infrequent enough that a day-long disk cache buys
little; an hour keeps scroll performance while bounding how long bytes
outlive a session on a shared workstation. Avatars and emoji keep their
longer cache.
sync_logs rows were never removed: no content, but an unbounded per-cycle
account of when each user's client was active. They now age out after
WAGGLE_SYNC_LOG_RETENTION_DAYS (default 90), pruned in the audit purge
loop's tick.
Mention-candidates (card and channel), resolve-mentions and people search
carried what the user types - and for resolve-mentions up to 2048 chars of
usernames naming who appears in a saved message - as GET query parameters,
which land verbatim in the uvicorn access log and in the ZAD ingress logs
we cannot scrub. All four are POST now, with the same validation moved into
request bodies; resolve-mentions and people take real lists instead of
comma-joined strings. Targets keeps GET (no user text).
The trailing path segment exists to title the browser's PDF viewer, but a
path lands verbatim in the uvicorn and ingress access logs, and filenames
are content. The URL now carries a fixed generic segment; the modal header
keeps showing the real name.
Reply-draft text, per-card visibility keys, and the recent-mention and
emoji lists survived logout in the browser profile; only the mention cache
was wiped. One sweep now runs next to it on both the logout and the 401
path. Theme and section-collapse stay: preferences, not personal traces.
The ZAD HAProxy ingress already records every request line at the edge; the
in-pod copy also carried query strings and real client IPs that the
redaction chain does not scrub. Dev and preview keep their access log.
New security.md section on response caching, browser storage, access logs
and the shared-workstation scenario; deployment.md gains WAGGLE_LOG_LEVEL
(never DEBUG in production) and WAGGLE_SYNC_LOG_RETENTION_DAYS rows.
A ValidationError always carries at least one error, so the empty-locs
fallback could never run; the >5-paths truncation now has the test it was
missing.
Ask the browser to drop the HTTP cache on logout
Some checks failed
CI / release-scripts (pull_request) Successful in 24s
security-scan / SBOM (trivy) (pull_request) Successful in 29s
security-scan / Filesystem scan (trivy fs) (pull_request) Failing after 29s
security-scan / JS SCA (npm audit) (pull_request) Failing after 30s
security-scan / Python SAST (bandit) (pull_request) Successful in 33s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 51s
test-build / build (frontend) (pull_request) Successful in 1m9s
test-build / build (backend) (pull_request) Successful in 1m26s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 3m16s
CI / frontend-test (pull_request) Successful in 4m32s
CI / e2e (pull_request) Failing after 11m22s
CI / backend-test (pull_request) Has been cancelled
04d7582faa
The proxied attachment bytes live in the browser's disk cache for up to an
hour; Clear-Site-Data: "cache" purges them on the deliberate logout in
Chromium and Firefox. "cache" only, so theme and layout preferences keep
surviving logout on purpose.
robbertbos force-pushed phase-0-leak-hygiene from 04d7582faa
Some checks failed
CI / release-scripts (pull_request) Successful in 24s
security-scan / SBOM (trivy) (pull_request) Successful in 29s
security-scan / Filesystem scan (trivy fs) (pull_request) Failing after 29s
security-scan / JS SCA (npm audit) (pull_request) Failing after 30s
security-scan / Python SAST (bandit) (pull_request) Successful in 33s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 51s
test-build / build (frontend) (pull_request) Successful in 1m9s
test-build / build (backend) (pull_request) Successful in 1m26s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 3m16s
CI / frontend-test (pull_request) Successful in 4m32s
CI / e2e (pull_request) Failing after 11m22s
CI / backend-test (pull_request) Has been cancelled
to 746cd3c0fe
Some checks failed
CI / release-scripts (pull_request) Successful in 6s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Failing after 15s
security-scan / JS SCA (npm audit) (pull_request) Failing after 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 17s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 37s
test-build / build (frontend) (pull_request) Successful in 54s
CI / frontend-test (pull_request) Successful in 1m1s
test-build / build (backend) (pull_request) Successful in 1m4s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 1m47s
CI / backend-test (pull_request) Successful in 2m32s
CI / e2e (pull_request) Failing after 4m38s
2026-08-15 12:53:56 +00:00
Compare
robbertbos force-pushed phase-0-leak-hygiene from 746cd3c0fe
Some checks failed
CI / release-scripts (pull_request) Successful in 6s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Failing after 15s
security-scan / JS SCA (npm audit) (pull_request) Failing after 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 17s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 37s
test-build / build (frontend) (pull_request) Successful in 54s
CI / frontend-test (pull_request) Successful in 1m1s
test-build / build (backend) (pull_request) Successful in 1m4s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 1m47s
CI / backend-test (pull_request) Successful in 2m32s
CI / e2e (pull_request) Failing after 4m38s
to 65cda0de63
Some checks failed
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 13s
security-scan / Filesystem scan (trivy fs) (pull_request) Failing after 16s
security-scan / JS SCA (npm audit) (pull_request) Failing after 21s
security-scan / Python SAST (bandit) (pull_request) Successful in 22s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 42s
CI / frontend-test (pull_request) Successful in 1m22s
CI / pre-commit (pull_request) Successful in 1m38s
test-build / build (frontend) (pull_request) Successful in 1m36s
test-build / build (backend) (pull_request) Successful in 1m56s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 7m52s
CI / e2e (pull_request) Failing after 8m23s
2026-08-15 13:07:54 +00:00
Compare
robbertbos force-pushed phase-0-leak-hygiene from 65cda0de63
Some checks failed
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 13s
security-scan / Filesystem scan (trivy fs) (pull_request) Failing after 16s
security-scan / JS SCA (npm audit) (pull_request) Failing after 21s
security-scan / Python SAST (bandit) (pull_request) Successful in 22s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 42s
CI / frontend-test (pull_request) Successful in 1m22s
CI / pre-commit (pull_request) Successful in 1m38s
test-build / build (frontend) (pull_request) Successful in 1m36s
test-build / build (backend) (pull_request) Successful in 1m56s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 7m52s
CI / e2e (pull_request) Failing after 8m23s
to 44367ba486
All checks were successful
CI / release-scripts (pull_request) Successful in 6s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 17s
security-scan / Python SAST (bandit) (pull_request) Successful in 18s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 36s
test-build / build (frontend) (pull_request) Successful in 56s
CI / pre-commit (pull_request) Successful in 57s
CI / frontend-test (pull_request) Successful in 59s
test-build / build (backend) (pull_request) Successful in 1m5s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 2m18s
CI / e2e (pull_request) Successful in 3m27s
2026-08-15 20:01:27 +00:00
Compare
robbertbos force-pushed phase-0-leak-hygiene from 44367ba486
All checks were successful
CI / release-scripts (pull_request) Successful in 6s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 17s
security-scan / Python SAST (bandit) (pull_request) Successful in 18s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 36s
test-build / build (frontend) (pull_request) Successful in 56s
CI / pre-commit (pull_request) Successful in 57s
CI / frontend-test (pull_request) Successful in 59s
test-build / build (backend) (pull_request) Successful in 1m5s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 2m18s
CI / e2e (pull_request) Successful in 3m27s
to bc7fea65c7
Some checks failed
CI / release-scripts (pull_request) Successful in 23s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 44s
security-scan / Python SAST (bandit) (pull_request) Successful in 51s
security-scan / JS SCA (npm audit) (pull_request) Successful in 54s
security-scan / SBOM (trivy) (pull_request) Successful in 47s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 1m10s
CI / backend-test (pull_request) Failing after 1m42s
CI / pre-commit (pull_request) Successful in 2m42s
test-build / build (frontend) (pull_request) Successful in 2m13s
CI / frontend-test (pull_request) Successful in 2m51s
test-build / build (backend) (pull_request) Successful in 2m36s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 5m21s
2026-08-15 20:24:50 +00:00
Compare
robbertbos force-pushed phase-0-leak-hygiene from bc7fea65c7
Some checks failed
CI / release-scripts (pull_request) Successful in 23s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 44s
security-scan / Python SAST (bandit) (pull_request) Successful in 51s
security-scan / JS SCA (npm audit) (pull_request) Successful in 54s
security-scan / SBOM (trivy) (pull_request) Successful in 47s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 1m10s
CI / backend-test (pull_request) Failing after 1m42s
CI / pre-commit (pull_request) Successful in 2m42s
test-build / build (frontend) (pull_request) Successful in 2m13s
CI / frontend-test (pull_request) Successful in 2m51s
test-build / build (backend) (pull_request) Successful in 2m36s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Successful in 5m21s
to 53f612b964
Some checks failed
CI / release-scripts (pull_request) Successful in 13s
security-scan / SBOM (trivy) (pull_request) Successful in 10s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 16s
security-scan / JS SCA (npm audit) (pull_request) Successful in 19s
security-scan / Python SAST (bandit) (pull_request) Successful in 20s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 41s
CI / frontend-test (pull_request) Failing after 1m7s
CI / pre-commit (pull_request) Successful in 1m21s
test-build / build (frontend) (pull_request) Successful in 1m6s
test-build / build (backend) (pull_request) Successful in 1m15s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 3m27s
CI / e2e (pull_request) Successful in 4m16s
2026-08-15 20:30:29 +00:00
Compare
robbertbos force-pushed phase-0-leak-hygiene from 53f612b964
Some checks failed
CI / release-scripts (pull_request) Successful in 13s
security-scan / SBOM (trivy) (pull_request) Successful in 10s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 16s
security-scan / JS SCA (npm audit) (pull_request) Successful in 19s
security-scan / Python SAST (bandit) (pull_request) Successful in 20s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 41s
CI / frontend-test (pull_request) Failing after 1m7s
CI / pre-commit (pull_request) Successful in 1m21s
test-build / build (frontend) (pull_request) Successful in 1m6s
test-build / build (backend) (pull_request) Successful in 1m15s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 3m27s
CI / e2e (pull_request) Successful in 4m16s
to ff2ceeabfa
All checks were successful
CI / release-scripts (pull_request) Successful in 7s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 17s
security-scan / Python SAST (bandit) (pull_request) Successful in 18s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
test-build / build (frontend) (pull_request) Successful in 56s
CI / pre-commit (pull_request) Successful in 57s
CI / frontend-test (pull_request) Successful in 59s
test-build / build (backend) (pull_request) Successful in 1m5s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 2m12s
CI / e2e (pull_request) Successful in 3m24s
CI / release-scripts (push) Successful in 5s
security-scan / SBOM (trivy) (push) Successful in 9s
security-scan / Filesystem scan (trivy fs) (push) Successful in 16s
security-scan / Python SAST (bandit) (push) Successful in 17s
security-scan / JS SCA (npm audit) (push) Successful in 17s
security-scan / Python SCA (pip-audit) (push) Successful in 35s
CI / pre-commit (push) Successful in 58s
CI / frontend-test (push) Successful in 59s
publish-main / build (frontend) (push) Successful in 1m9s
publish-main / build (backend) (push) Successful in 1m32s
publish-main / build (push) Successful in 0s
CI / backend-test (push) Successful in 2m18s
CI / e2e (push) Successful in 3m24s
2026-08-15 20:36:00 +00:00
Compare
robbertbos deleted branch phase-0-leak-hygiene 2026-08-15 20:39:55 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
robbertbos/waggle!313
No description provided.