[Vulnerability] Hardcoded JWT Secret May Lead to Privilege Escalation Risks
Description:
This project is an open source project, and any user can access the hard coded JWT Secret in this project. At the same time, there are no warning prompts when starting the project using the default JWT Secret value, so most users may not modify this JWT Secret default value, which may allow attackers to forge arbitrary user permission tokens, thereby bypassing authentication and authorization mechanisms and accessing protected interfaces.
Key Location:
Vulnerability Type:
| Type | Description |
|---|---|
| Hardcoded Secret | A JWT signing secret is hardcoded in the project configuration. |
| JWT Secret Disclosure | The signing secret is publicly accessible. |
| JWT Forgery | Attackers can generate valid JWTs. |
| Authentication Bypass | Forged tokens can bypass identity verification. |
| Authorization Bypass | Forged tokens may bypass access control. |
| User Impersonation | Attackers can impersonate arbitrary users. |
| Broken Access Control | Role-based access control is not effectively enforced. |
| CWE-798 | Use of Hard-coded Credentials |
| CWE-321 | Use of Hard-coded Cryptographic Key |
Severity:
High
Exploitability Analysis:
The project hardcodes the JWT Secret in docker-compose.yml:12 as a literal value: JWT_SECRET: 0a6b944d-d2fb-46fc-a85e-0295c986cd9f.
This secret is injected into the container through Docker Compose environment variables, then read from process.env, validated through Joi in config.js, and assigned to config.jwtSecret. It eventually flows into the JwtStrategy secretOrKey verification sink in passport.js. The README recommends docker-compose up -d as the default deployment method, which may cause this default hardcoded secret to be used in real deployments.
JWT Signing and Verification Flow
| Stage | Location / Component | Description |
|---|---|---|
| Secret Definition | docker-compose.yml:12 |
JWT_SECRET is hardcoded as a fixed string. |
| Environment Injection | Docker Compose | The secret is injected into the container environment. |
| Configuration Loading | config.js |
The secret is read from process.env and assigned to config.jwtSecret. |
| Token Verification | passport.js |
JwtStrategy.secretOrKey uses this secret to verify JWT signatures. |
| Default Deployment | README |
docker-compose up -d may start the service with the default secret. |
Payload Analysis
Production tokens are generated by generateToken, which signs the entire user object into the JWT payload via JSON.stringify(user). The payload includes:
| Field | Meaning | Security Analysis |
|---|---|---|
_id |
User ID | Returned in registration and login responses; a persistent identifier obtainable by attackers. |
fullname |
Full name | User profile field that may be obtained or forged. |
email |
Email address | An enumerable or obtainable identity identifier. |
createdAt |
Creation timestamp | Low-sensitivity metadata that can be forged. |
roles |
User roles | User-supplied field with potential forgery risk. |
These fields are obtainable, enumerable, or forgeable by attackers. The payload does not contain any server-generated high-entropy value that is validated during token verification.
Token Verification Logic
The passport-jwt verify callback decodes the JWT and only uses payload._id to query the database: User.findById(payload._id).
If the corresponding user exists, the server sets the database user object as req.user, which is then trusted by downstream business logic as the authenticated identity. The validation flow does not verify server-side session state, token version, jti, nonce, or any other non-forgeable server-side value.
Therefore, the token model is semi-stateful: besides signature verification, the server only checks whether the database user corresponding to payload._id exists. Since _id is an attacker-obtainable persistent identifier, this check does not effectively prevent user impersonation.
Authorization Control Issue
Although require-admin.js defines an administrator authorization middleware that checks req.user.roles, it is not imported or used in any route file. This makes the authorization middleware dead code, leaving the backend without effective role-based access control. This further amplifies the impact of JWT forgery.
Once an attacker obtains the hardcoded secret 0a6b944d-d2fb-46fc-a85e-0295c986cd9f, they can forge a JWT containing any known valid _id. The forged token can pass signature verification and identity lookup, and will be accepted by the application as a legitimate authenticated user. The attacker does not need the target user's password to impersonate that user and access protected resources.
Impact:
- Attackers can forge JWTs for arbitrary users
- Attackers can impersonate administrators or privileged accounts to access sensitive APIs
- The user authentication mechanism becomes ineffective
- This may lead to data leakage, unauthorized operations, or account takeover
Fix:
JWT Secret can be placed in the system environment variable or disabled from starting projects with default values.
Source: linnovate/mean