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(defaultsecretvalue copied verbatim whenconfig.yamlis absent)utils/bootstrap.go(weak key only warns; does not refuse to start)auth/auth.goGenerateToken()/ParseToken()(sign/verify with the same config key)connection/database.goInitRootUser()(auto-createsrootadmin with hard-coded default password)
Reproduction (verified on official docker-compose deployment)
- Deploy per README without setting
SECRETand without editing the config:The container logs:cd chatnio && docker compose up -dconfig.yaml not found, creating one from template, followed by the weak-key warning, then normal startup and creation of therootadmin. - Build a JWT with claims
username=root,password=<sha256 of the hard-coded default root password>, and a futureexp, signed with the default config key. - Send it as the
Authorizationheader:POST /api/state -> {"admin":true,"status":true,"user":"root"} GET /api/admin/user/list?page=0 -> HTTP 200, lists all users - 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 returnsHTTP 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
SECRETin 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.
Source: coaidev/coai