Skip to content

Fixed JWT_SECRET allows authentication bypass and remote code execution #41

Description

@28Hus

Summary

The web server signs and verifies JSON Web Tokens with a fixed, publicly-known secret. Under the official deployment flow, the effective JWT_SECRET is a constant that ships in the repository — either the placeholder change-this-to-a-random-secret-string from .env.example, or the hardcoded fallback deepbot-default-secret-change-in-production when JWT_SECRET is unset. Anyone who knows that constant can forge a valid JWT for any userId, bypass the ACCESS_PASSWORD login check, and reach every protected API and the WebSocket. Because the same constant is reused as the X-Secret for /api/external, it also unlocks the external API. From there an attacker can drive the AI agent's bash tool to execute arbitrary commands on the server (verified in a local Docker deployment of this project, run as root).

Verdict: Confirmed via dynamic reproduction under the official Docker/Web runtime (node:22, NODE_ENV=production, DEEPBOT_DOCKER=true, .env.example defaults).

Affected components / prerequisites

  • src/server/middleware/auth.ts (lines 14, 39, 51)
  • src/server/websocket-manager.ts (lines 20, 105)
  • src/server/routes/external.ts (lines 17, 107)
  • .env.example (line 13)
  • Prerequisites: service is network-reachable (docker-compose publishes 0.0.0.0:3008 by default). Attackers do not need any account or prior access.

Root cause

The JWT signing/verification secret is a public constant in every official default variant:

// src/server/middleware/auth.ts:14
const JWT_SECRET = process.env.JWT_SECRET || 'deepbot-default-secret-change-in-production';
# .env.example:13   (documented flow copies this to .env)
JWT_SECRET=change-this-to-a-random-secret-string

The same constant gates /api/external:

// src/server/routes/external.ts:17, 99-113
const JWT_SECRET = process.env.JWT_SECRET || 'deepbot-default-secret-change-in-production';
// if (secret !== JWT_SECRET) { 403 }

authMiddleware trusts any token that verifies with this constant:

// src/server/middleware/auth.ts:37-44
const token = authHeader.replace('Bearer ', '');
const decoded = jwt.verify(token, JWT_SECRET) as TokenPayload;   // line 39
(req as AuthRequest).userId = decoded.userId;
next();

Impact

  • Authentication bypass: forge {userId: "default"} with the known constant (30-day expiry, matching generateToken) and send Authorization: Bearer <token>GET /api/config returns 200 (no token returns 401). Applies to all /api/* endpoints mounted behind authMiddleware (config, tabs, tools, connectors, files, skills, tasks, token/image usage) and to the WebSocket (/ws?token=...).
  • External API bypass: X-Secret: <known constant> passes the /api/external gate (missing header → 401, wrong value → 403, known value → accepted).
  • Remote code execution: with a configured AI model (required by the official .env), a request through /api/tabs/:id/messages or /api/external/command reaches the agent, which can invoke the bash tool and execute arbitrary shell commands as root in the container. Verified locally: id, hostname, whoami, pwd all executed. The command path-check only confines file paths referenced inside a command to /data and /tmp; commands without such paths run unrestricted, and the dangerous-command blocklist is trivially bypassed.
  • Data / credentials: system config, Tab history, connector configurations (WeChat / WeCom / Feishu / smart-KF credentials) and pairing records are readable/writable.

Reproduction (on a controlled local deployment)

Set up per the documented flow (copy .env.example to .env, docker compose up; then set an access password if desired). In a test environment:

  1. Forge a token exactly as generateToken does, using the constant (e.g. jwt.sign({userId:'default'},'<JWT_SECRET>',{expiresIn:'30d'})).
  2. curl http://<host>:3008/api/config with no token → 401.
  3. curl -H "Authorization: Bearer <forged>" http://<host>:3008/api/config200 with the full system config.
  4. WebSocket: ws://<host>:3008/ws?token=<forged> connects and answers pong; without a token it closes with code 1008.
  5. /api/external/command with X-Secret: <constant> and a harmless command (hostname > /tmp/out; cat /tmp/out) returns success:true and the command output is written — arbitrary command execution.

Negative controls: a token forged with a random secret returns 401; with a strong random JWT_SECRET set by the operator, tokens forged with both known constants return 401 (confirming the root cause is the fixed/public secret, not the JWT verification logic). A wrong X-Secret returns 403.

Suggested remediation

  1. Remove all hardcoded JWT fallback values (auth.ts:14, websocket-manager.ts:20, external.ts:17); require JWT_SECRET and fail closed at startup when it is absent. Document generating a strong random key (e.g. openssl rand -base64 48).
  2. Purge the placeholder change-this-to-a-random-secret-string from the repository and make .env.example JWT_SECRET empty or required.
  3. Do not reuse the JWT secret as the /api/external X-Secret; use a separate, random, required key with rotation.
  4. Add server-side token revocation (token version / jti denylist) so leaked tokens can be invalidated.
  5. Reassess exposing the agent's bash (arbitrary shell) capability to network clients; confine execution to controlled directories with audit, or disable remote exec by default.

Notes / scope

  • Verified on a local, isolated Docker deployment (no real API key used; the model-call step was exercised against a local mock endpoint, while the bash tool and its spawn(shell:true) execution are this project's real code).
  • The Electron desktop client uses the same auth code paths but was not separately run.
  • I have not included a complete usable forged token in this issue.

Thanks for looking into this. Happy to provide additional detail or a reproduction environment.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions