NginxProxyManager / NginxProxyManager/nginx-proxy-manager
JWT bearer token stored in localStorage exposes all users to XSS-based session theft
Nobody has claimed this yet.
- 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:
- On
POST /api/tokens(login) andPOST /api/tokens/2fa, set:npm_session— HttpOnly, Secure (when behind HTTPS), SameSite=Strict — carries the JWTnpm_csrf— non-HttpOnly, SameSite=Strict — carries a random CSRF token readable by JS
- Add CSRF middleware that rejects all mutating requests (
POST/PUT/PATCH/DELETE) where theX-CSRF-Tokenheader does not match thenpm_csrfcookie value (timing-safe comparison). - Add
POST /api/tokens/logoutto clear both cookies server-side. - Keep
Authorization: Bearerheader support for API clients and CI pipelines (backward compat).
Frontend changes:
- Remove all
localStoragereads/writes fromAuthStore.ts. Replace with an in-memory boolean flag. - On app mount, fire a
GET /api/tokensprobe — 200 means a valid session cookie exists (mark logged in), 401 means no session (show login form). - Read the
npm_csrfcookie and send it asX-CSRF-Tokenon all mutating requests. - Call
POST /api/tokens/logouton logout so the server clears both cookies.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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