NginxProxyManager / NginxProxyManager/nginx-proxy-manager

JWT bearer token stored in localStorage exposes all users to XSS-based session theft

Open
#5,549 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
TypeScript
Stars
34.2k
Forks
3.9k
Avg merge
21h 12m
Merged PRs (30d)
20

Description

Summary

The admin UI stores the authenticated JWT bearer token in window.localStorage under the key authentications. Any JavaScript executing in the admin UI origin — including via a stored XSS, a malicious browser extension, or a supply-chain-compromised frontend dependency — can read and exfiltrate this token. A stolen token grants full API access with no additional authentication required.

Affected Versions

All versions of jc21/nginx-proxy-manager that include the React frontend. The vulnerability originates in frontend/src/modules/AuthStore.ts and has been present since the React UI was introduced.

Verified against: **v2.14.x

Root Cause

AuthStore.ts persists the JWT in localStorage:

// frontend/src/modules/AuthStore.ts
window.localStorage.setItem("authentications", JSON.stringify(stack));

localStorage is readable by any JavaScript on the same origin (http://<host>:81). The Authorization: Bearer <token> header is then sent on every API request by reading this value back:

// frontend/src/api/backend/base.ts
const token = AuthStore.getTopToken()?.t;
headers["Authorization"] = `Bearer ${token}`;

There is no HttpOnly flag, no SameSite scope, and no CSRF protection. The session is fully credential-equivalent and lives until the token expires (~1 day default).

Exploitation

Impact

Impact Detail
Authentication bypass Valid session token grants full API access without credentials
Privilege escalation Admin token grants access to all user management, certificate management, and nginx config endpoints
Persistent access Token valid for ~24 hours; attacker can create new admin accounts before expiry
Proxy takeover Attacker can create/modify/delete proxy hosts, redirections, SSL certificates, and access lists
Data exposure DNS provider API keys stored in certificate meta are accessible via the certificates API
Lateral movement Admin can configure proxy hosts to route traffic to internal/LAN services (SSRF)

Suggestion

Move the JWT out of localStorage and into an HttpOnly cookie inaccessible to JavaScript. Add CSRF double-submit protection to prevent cross-site request forgery now that the credential is cookie-bound.

Backend changes:

  1. On POST /api/tokens (login) and POST /api/tokens/2fa, set:
    • npm_session — HttpOnly, Secure (when behind HTTPS), SameSite=Strict — carries the JWT
    • npm_csrf — non-HttpOnly, SameSite=Strict — carries a random CSRF token readable by JS
  2. Add CSRF middleware that rejects all mutating requests (POST/PUT/PATCH/DELETE) where the X-CSRF-Token header does not match the npm_csrf cookie value (timing-safe comparison).
  3. Add POST /api/tokens/logout to clear both cookies server-side.
  4. Keep Authorization: Bearer header support for API clients and CI pipelines (backward compat).

Frontend changes:

  1. Remove all localStorage reads/writes from AuthStore.ts. Replace with an in-memory boolean flag.
  2. On app mount, fire a GET /api/tokens probe — 200 means a valid session cookie exists (mark logged in), 401 means no session (show login form).
  3. Read the npm_csrf cookie and send it as X-CSRF-Token on all mutating requests.
  4. Call POST /api/tokens/logout on logout so the server clears both cookies.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading frontend/src/modules/AuthStore.ts and frontend/src/api/backend/base.ts, then trace the POST /api/tokens, POST /api/tokens/2fa, GET /api/tokens, and logout flows. Done means sessions no longer store JWTs in localStorage, cookie-based requests enforce the proposed CSRF checks, logout clears both cookies, and Authorization bearer support remains available for API clients.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
api, authentication, backend, frontend, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.