#411·coai

Default JWT signing key is predictable, allowing admin token forgery in default deployments

Author: 28HusCreated Aug 26, 2026Updated Aug 26, 2026

Default JWT signing key is predictable, allowing admin token forgery in default deployments

Summary

In an official default deployment (the README's primary docker compose up -d path, using the official image and default config), the JWT signing key is a short fixed literal. utils/bootstrap.go only logs a warning and sleeps 10 seconds when the key is weak (invalid secret length) and then starts normally; none of the official docker-compose files sets SECRET. In addition, when the database is empty, connection/database.go auto-creates an administrator account root using the hard-coded default password from source (also documented in the README). Combined, this lets an attacker forge a valid admin JWT offline and take over the administrator account.

Affected components

  • config.example.yaml (default secret value copied verbatim when config.yaml is absent)
  • utils/bootstrap.go (weak key only warns; does not refuse to start)
  • auth/auth.go GenerateToken() / ParseToken() (sign/verify with the same config key)
  • connection/database.go InitRootUser() (auto-creates root admin with hard-coded default password)

Reproduction (verified on official docker-compose deployment)

  1. Deploy per README without setting SECRET and without editing the config:
    bash
    cd chatnio && docker compose up -d
    The container logs: config.yaml not found, creating one from template, followed by the weak-key warning, then normal startup and creation of the root admin.
  2. Build a JWT with claims username=root, password=<sha256 of the hard-coded default root password>, and a future exp, signed with the default config key.
  3. Send it as the Authorization header:
    POST /api/state  ->  {"admin":true,"status":true,"user":"root"}
    GET  /api/admin/user/list?page=0  ->  HTTP 200, lists all users
  4. Negative control — the same request with a token signed by any other key returns {"admin":false,"status":false,"user":""}; the admin endpoint without a token returns HTTP 401.

Impact

  • Full administrator takeover in default deployments: user management, password resets, quota/subscription grants, invite/redeem codes, logs, channel configuration.
  • Any captured user JWT can be re-signed indefinitely with the default key, bypassing the 30-day expiry.

Remediation

  • Refuse to start (or auto-generate and persist a random key) when the secret is weak; remove the fixed default value from config.example.yaml.
  • Stop hard-coding the initial admin password; generate a random one on first boot and print a one-time credential.
  • Require an explicitly injected random SECRET in the official docker-compose files and README examples.

Scope and limitations

  • Verified in a local isolated deployment (official image + default config). No production systems were touched.
  • If an operator explicitly sets a strong secret and changes the default admin password, this issue does not apply.
  • A separate related issue covers the password hash embedded in JWTs.