Skip to content

fix: stop exposing the Knox session token to page JavaScript - #314

Open
TheoBhang wants to merge 2 commits into
thalesgroup-cert:masterfrom
TheoBhang:fix/knox-token-httponly-cookie
Open

fix: stop exposing the Knox session token to page JavaScript#314
TheoBhang wants to merge 2 commits into
thalesgroup-cert:masterfrom
TheoBhang:fix/knox-token-httponly-cookie

Conversation

@TheoBhang

Copy link
Copy Markdown

Problem

The SPA persisted its Knox auth token in localStorage (reducers/auth.js), and for SSO logins the OIDC callback handed the raw token to the browser via a ?sso_token=... query parameter before the SPA moved it into localStorage (accounts/oidc_views.py, frontend/src/components/App.js).

Both are readable by any script running on the page — a single XSS finding (stored, reflected, or via a compromised npm dependency) is a full account takeover, no separate bug needed. The URL-param handoff also means the raw token briefly transits as a query string: visible in the OIDC callback's 302 Location header, in browser history until window.history.replaceState fires, and in any proxy/access log sitting in front of the deployment.

Fix

Mirrors the pattern already shipped on Thales CERT's Suspicious platform (api/authentication.py::KnoxCookieAuthentication / _set_auth_cookies):

  • accounts/authentication.py (new): KnoxCookieAuthentication, reading the token from an httpOnly, SameSite=Strict cookie. Falls through to header-based knox.auth.TokenAuthentication when the cookie is absent, so existing non-browser API clients using the Authorization header are unaffected.
  • accounts/api.py: LoginAPI now sets the cookie on login (in addition to the existing JSON token field, kept for non-browser clients). Added LogoutAPI to clear both cookies and delete the token server-side — knox's stock LogoutView has no notion of the cookie, so it was routed ahead of knox.urls's own logout path in accounts/urls.py.
  • accounts/oidc_views.py: SSOCallbackView now sets the cookie server-side and redirects to /?sso=1 — the token no longer appears in the URL at all.
  • watcher/settings.py: KnoxCookieAuthentication registered ahead of header auth in REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES.
  • Frontend: reducers/auth.js, actions/auth.js, and components/App.js no longer read/write the token via localStorage. Also caught and fixed two more call sites that would otherwise have silently broken — contexts/ThemeContext.js and services/preferencesService.js were independently reading localStorage.getItem('token') to hand-build Authorization headers for theme/preference sync; both now rely on the cookie like everything else.

Unrelated build fix bundled in

While testing this through a full local deployment, discovered npm run build has been silently broken (reproduced against the unmodified base commit too, not caused by this change): webpack fails with ERR_REQUIRE_ESM because babel-loader@10.1.1 does a synchronous require() of @babel/core@8.0.1, which ships as pure ESM. Root cause: @babel/core@8's own transitive deps require Node ^20.19.0 || >=22.12.0 (synchronous require(esm) support), but the Dockerfile's frontend-builder stage was pinned to node:18-bookworm-slim. Fixed by bumping to node:22-bookworm-slim and adding an explicit engines.node constraint to package.json — confirmed this alone fixes the build with zero package downgrades. Rebuilt and committed the frontend bundle, which is also the first rebuild since this fix, so it's the first bundle that's actually free of the localStorage token usage above.

Test plan

  • python manage.py test accounts — new KnoxCookieAuthTest (login sets both cookies, cookie alone authenticates with no Authorization header, logout clears both and invalidates the token) plus all pre-existing accounts tests, run inside the built image against real MySQL: 8/8 passing.
  • python manage.py test (full suite) run the same way: 250/250 passing, no regressions.
  • Built the actual Docker image from this branch and deployed it (LDAP-backed auth against a real directory) — live-verified with curl: login sets Set-Cookie: knox_token=...; HttpOnly; SameSite=Strict, GET /api/auth/user authenticates via cookie alone (no Authorization header sent), logout clears both cookies, and a replayed post-logout cookie correctly gets 401.
  • Confirmed the shipped JS bundle no longer contains loginWithToken, sso_token, or any localStorage-token pairing.

The SPA persisted its Knox auth token in localStorage (reducers/auth.js)
and, for SSO logins, the OIDC callback handed the raw token to the
browser via a `?sso_token=...` query param before the SPA moved it into
localStorage. Both are readable by any script on the page, so a single
XSS finding is a full account takeover.

Mirrors the fix already shipped on the Suspicious platform
(api/authentication.py::KnoxCookieAuthentication / _set_auth_cookies):

- accounts/authentication.py: new KnoxCookieAuthentication, reading the
  token from an httpOnly, SameSite=Strict cookie, falling through to
  header-based knox.auth.TokenAuthentication for non-browser clients.
  Registered ahead of it in REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES.
- accounts/api.py: LoginAPI now sets the cookie on login; added LogoutAPI
  to clear it (knox's stock LogoutView has no notion of it), routed ahead
  of knox.urls's own logout path.
- accounts/oidc_views.py: SSOCallbackView now sets the cookie server-side
  and redirects to /?sso=1 instead of putting the token in the URL.
- frontend: reducers/auth.js, actions/auth.js, components/App.js,
  contexts/ThemeContext.js, and services/preferencesService.js no longer
  read/write the token via localStorage or attach it as an Authorization
  header by hand — the browser sends the httpOnly cookie automatically on
  every same-origin request.

Added accounts/tests.py::KnoxCookieAuthTest covering: login sets both
cookies, the cookie alone authenticates a request with no Authorization
header, and logout clears both cookies and invalidates the token.

Not pushed — local commit only.
npm run build has been silently broken on this branch (and on master,
confirmed by reproducing against the unmodified base commit): webpack
fails with `ERR_REQUIRE_ESM` because babel-loader@10.1.1 does a
synchronous require() of @babel/core@8.0.1, which ships as pure ESM.
babel-loader@10.1.1 is already the latest published release, so this
isn't a version to bump away from.

Root cause is simpler than it looks: @babel/core@8's own transitive
deps (babel-plugin-polyfill-corejs3, lru-cache) declare
engines.node: "^20.19.0 || >=22.12.0" — synchronous require(esm) support
only landed in Node at that point. The Dockerfile's frontend-builder
stage was still pinned to node:18-bookworm-slim. Confirmed by building
with node:22-bookworm-slim against the exact same unmodified
package.json/package-lock.json — npm run build succeeds cleanly, no
package downgrades needed.

- Watcher/Dockerfile: frontend-builder base image 18 -> 22-bookworm-slim.
- Watcher/package.json: added engines.node >=20.19.0 so this shows up as
  an explicit, readable constraint instead of only being discoverable
  via EBADENGINE warnings.
- Regenerated frontend/static/frontend/main.js(.map): this repo commits
  the built bundle rather than building it as part of `docker build`
  (the frontend-builder stage installs deps but never actually calls
  `npm run build` — a separate pre-existing gap, not touched here), so
  the fix isn't real until the checked-in bundle is rebuilt with it.
  This also happens to be the first bundle rebuilt since the
  httpOnly-cookie fix (1e371fb) landed, which is now what's actually
  served — verified live: POST /api/auth/login sets an HttpOnly
  SameSite=Strict cookie, GET /api/auth/user authenticates off the
  cookie alone, logout clears it, and the shipped bundle no longer
  touches localStorage for the token.

Not pushed — local commit only.
@TheoBhang
TheoBhang requested a review from ygalnezri July 23, 2026 14:49
@TheoBhang TheoBhang added bug Something isn't working enhancement New feature or request dependencies Pull requests that update a dependency file python Pull requests that update Python code vulnerability Something is vulnerable labels Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working dependencies Pull requests that update a dependency file enhancement New feature or request python Pull requests that update Python code vulnerability Something is vulnerable

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant