Skip to content

fix(dns-webui): store JWTs in HttpOnly cookies, not localStorage (XSS hardening) - #97

Merged
PenguinzTech merged 1 commit into
v2.1.xfrom
fix/dns-webui-httponly-cookies
Sep 2, 2026
Merged

fix(dns-webui): store JWTs in HttpOnly cookies, not localStorage (XSS hardening)#97
PenguinzTech merged 1 commit into
v2.1.xfrom
fix/dns-webui-httponly-cookies

Conversation

@PenguinzTech

Copy link
Copy Markdown
Contributor

Summary

dns-webui stored access + refresh JWTs in localStorage, so any JavaScript (app or a compromised dependency) could exfiltrate them (persistent account takeover via the refresh token). Moved to HttpOnly, Secure, SameSite=Strict cookies that JS cannot read. The change is additive — bearer-token clients (manager/frontend, Go client) are unaffected.

Backend (manager)

  • app/services/cookie_auth.py (new): sets/clears HttpOnly+Secure+SameSite=Strict access_token/refresh_token cookies + a JS-readable csrf_token cookie; double-submit CSRF check (secrets.compare_digest).
  • middleware/auth.py: token_required accepts the cookie as a fallback to the Authorization header; enforces CSRF on cookie-sourced mutating requests only (header clients exempt).
  • blueprints/auth.py (login/refresh/logout), mfa.py (mfa_verify): set cookies alongside the existing JSON tokens; logout clears cookies + revokes the cookie-sourced refresh token.
  • config.py: COOKIE_SECURE/COOKIE_DOMAIN (dev/test default to non-Secure for plain HTTP).

Frontend (dns-webui)

  • Removed all localStorage token read/write; API uses withCredentials: true and auto-attaches the CSRF header on mutating requests; 401 → cookie-based refresh.
  • useAuth tracks auth state via /auth/me, never holding the JWT in JS. document.cookie cannot see the HttpOnly token cookies.

Why cookies + CSRF (not just cookies)

A cookie-auth flow without CSRF protection trades XSS for CSRF, so the double-submit token is part of the fix, not an add-on.

Verification

  • 16 new backend cookie/CSRF tests; backend suite 492 passed (1 pre-existing env-only observability failure). dns-webui vitest 9 passed. flake8 clean.

Pre-existing issues surfaced (NOT introduced here, not fixed)

  • tsc/vite build fail on @penguintechinc/react-libs's package.json exports["."] ordering (import before types) — breaks moduleResolution: bundler for every consumer; fix belongs in penguin-libs. Confirmed identical error on untouched files.
  • npm run lint fails: eslint referenced but not a devDependency.
  • dns-webui serves via nginx (not Express) and vite.config.ts/nginx.conf still point at a legacy flask-api target that no longer exists — dead config, separate cleanup.

🤖 Generated with Claude Code

… hardening)

dns-webui kept access + refresh JWTs in localStorage, so any JavaScript (app or a
compromised dependency) could exfiltrate them. Moved to HttpOnly, Secure,
SameSite=Strict cookies that JS cannot read. Change is ADDITIVE — bearer-token
clients (manager/frontend, Go client) are unaffected.

Backend (manager):
- app/services/cookie_auth.py (new): set/clear HttpOnly+Secure+SameSite=Strict
  access/refresh cookies + a JS-readable csrf_token cookie; double-submit CSRF
  check (secrets.compare_digest).
- middleware/auth.py: token_required accepts the cookie as a fallback to the
  Authorization header; enforces CSRF on cookie-sourced mutating requests only
  (header clients exempt).
- blueprints/auth.py (login/refresh/logout), mfa.py (mfa_verify): set cookies
  alongside the existing JSON tokens; logout clears cookies + revokes the
  cookie-sourced refresh token.
- config.py: COOKIE_SECURE/COOKIE_DOMAIN (dev/test default to non-Secure).

Frontend (dns-webui):
- Removed ALL localStorage token read/write; api uses withCredentials:true and
  auto-attaches the CSRF header on mutating requests; 401 -> cookie-based refresh.
- useAuth tracks auth state via /auth/me, never holding the JWT in JS.

Tests: 16 backend cookie/CSRF tests + updated frontend Login test. Backend suite
492 passed (1 pre-existing env-only observability failure); dns-webui vitest 9
passed. flake8 clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PenguinzTech PenguinzTech self-assigned this Sep 2, 2026
@PenguinzTech
PenguinzTech merged commit 4fe6969 into v2.1.x Sep 2, 2026
16 of 17 checks passed
@PenguinzTech
PenguinzTech deleted the fix/dns-webui-httponly-cookies branch September 2, 2026 19:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant