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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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