Patch notes — security hardening (stored XSS, mobile IDOR, headers) (2026-06-18)
Branch: dev (not yet merged to main, so not deployed — main is the prod trigger).
Six fixes from an authorized security test of the admin app, driven against a local
full stack on localhost:8080. Full write-up:
../Bugfix/bugfix-report-2026-06-18-security-hardening.md.
Docker smoke 157/0 on the dev tree.
1 — Avatar stored XSS → admin takeover (Critical) ✅
The /account avatar accepted any data:image/…-prefixed string and rendered it raw into
a style="…url('…')" attribute shown cross-user (an author’s avatar on the admin’s ticket
thread) — a customer could store a payload that ran in the admin’s browser (confirmed live).
Now validated as a strict base64 image data URL at the store and esc()-escaped at every
render sink. Commit 429b5d3.
2 — Mobile ticket IDOR (Critical) ✅
/mobile/ticket let a customer read any ticket in their company, dropping the
company_admin gate the desktop view enforces. Routed through the shared
_customer_ticket() boundary. Commit 5ef94bc.
3 — Username stored XSS on the admin Users page (High) ✅
User creation now constrains usernames to [A-Za-z0-9._-]{1,64} (they’re interpolated into
form="u_<name>" markup; a company_admin customer could reach one of the creation paths).
Commit 429b5d3.
4 — Hardening headers + Secure cookie (Low) ✅
X-Content-Type-Options: nosniff on every response; X-Frame-Options: SAMEORIGIN +
Referrer-Policy on HTML; session cookie marked Secure on HTTPS (via X-Forwarded-Proto
from the Traefik edge; unset on plain HTTP so dev/smoke still work). Commit da7730e.
5 — Constant-time collector token (Low) ✅
/api/collector/push now compares the token with hmac.compare_digest. Commit 4602f70.
Deferred (tracked): SSRF guard for monitor/api-source/deployment URLs (High; needs a
shared assert_public_url() + moving those routes into ADMIN_ONLY_PATHS); invoice
company-scoping (Medium; matches the documented v1 “staff = all companies” model). No
access control was weakened. Files changed: ethica/handlers/{account,mobile,public,settings,base,auth}.py,
ethica/ui/html.py.