no-hardcoded-secret
The rule provides LLM-optimized error messages (Compact 2-line format) with actionable security guidance:
Disallow hardcoded secrets in JWT sign/verify operations
🚨 Security rule | 💡 Provides LLM-optimized guidance | ⚠️ Set to error in recommended
Quick Summary
| Aspect | Details |
|---|---|
| CWE Reference | CWE-798 |
| Severity | Critical |
| Auto-Fix | ❌ No auto-fix available |
| Category | Security |
| ESLint MCP | ✅ Optimized for ESLint MCP integration |
| Best For | Protecting JWT secrets from source code exposure |
Error Message Format
The rule provides LLM-optimized error messages (Compact 2-line format) with actionable security guidance:
🔒 CWE-798 OWASP:A04 CVSS:9.8 | Hardcoded Credentials detected | CRITICAL [SOC2,PCI-DSS,HIPAA,GDPR,ISO27001,NIST-CSF]
Fix: Review and apply the recommended fix | https://owasp.org/Top10/A04_2021/Message Components
| Component | Purpose | Example |
|---|---|---|
| Risk Standards | Security benchmarks | CWE-798 OWASP:A04 CVSS:9.8 |
| Issue Description | Specific vulnerability | Hardcoded Credentials detected |
| Severity & Compliance | Impact assessment | CRITICAL [SOC2,PCI-DSS,HIPAA,GDPR,ISO27001,NIST-CSF] |
| Fix Instruction | Actionable remediation | Follow the remediation steps below |
| Technical Truth | Official reference | OWASP Top 10 |
Rule Details
This rule detects hardcoded secrets in source code. Secrets committed to repositories can be extracted by anyone with code access.
Examples
❌ Incorrect
// Hardcoded string
jwt.sign(payload, 'my-secret-key');
jwt.verify(token, 'my-secret-key');
// Template literal
jwt.sign(payload, `static-secret`);
// A fallback that ships the literal the moment the env var is unset
jwt.sign(payload, process.env.JWT_SECRET || 'secret');
const SECRET = process.env.JWT_SECRET ?? 'changeme';
// jose: the byte key, inline or one const away, including SignJWT(...).sign(key)
const key = new TextEncoder().encode('secret');
await new SignJWT(claims).setProtectedHeader({ alg: 'HS256' }).sign(key);
// Config-object APIs
JwtModule.register({ secret: 'secretKey' }); // @nestjs/jwt
this.jwtService.sign(payload, { secret: 'secretKey' }); // @nestjs/jwt per call
expressjwt({ secret: 'shhhhhhared-secret', algorithms: ['HS256'] }); // express-jwt
new JwtStrategy({ secretOrKey: 'secret', jwtFromRequest }, verify); // passport-jwt
createSigner({ key: 'secret' }); // fast-jwt✅ Correct
// Environment variable
jwt.sign(payload, process.env.JWT_SECRET);
// Config object
jwt.sign(payload, config.jwtSecret);
// Function call
jwt.sign(payload, getSecretFromVault());
// A PEM PUBLIC key or certificate is published on purpose — not a secret
const IDP_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----`;
jwt.verify(token, IDP_PUBLIC_KEY, { algorithms: ['ES256'] });
// Test files are skipped: a fixture token signed with 'test-secret' ships nothingCalls that only share a method name are not JWT calls: a createSign() /
createVerify() object from node:crypto, WebCrypto's crypto.subtle, and
an injected this.<member> the class gives no evidence for. A member is a JWT
client only when its type annotation resolves to a JWT library
(private readonly jwt: JwtService), it is @Inject(X)-ed with a JWT import,
or it is assigned from one (= new JwtService(), this.lib = jsonwebtoken).
An untyped constructor(private readonly hashing) is not assumed to be one.
The key is followed within the file: a const, a let that is never
reassigned, a destructure, a member of an object literal
(config.jwt.secret), and the single return of a same-file function.
Known False Negatives
The following patterns are not detected due to static analysis limitations:
Dynamic Secret Construction
Why: The rule checks for literal strings but cannot evaluate runtime string operations.
// ❌ NOT DETECTED - String built at runtime
const prefix = 'secret';
const suffix = '-key-123';
jwt.sign(payload, prefix + suffix); // Concatenation not statically resolvedMitigation: Use ESLint's prefer-template and code review. Consider runtime secret detection.
Encoded/Obfuscated Secrets
Why: The rule matches plain text literals, not decoded values.
// ❌ NOT DETECTED - Base64 encoded secret
const secret = Buffer.from('c2VjcmV0LWtleQ==', 'base64').toString();
jwt.sign(payload, secret);Mitigation: Use pre-commit hooks that scan for base64-encoded secrets. Consider tools like git-secrets.
Cross-Module Secret Passing
Why: ESLint analyzes one file at a time; imported values cannot be traced.
// ❌ NOT DETECTED - Secret imported from another file
// secrets.ts: export const JWT_SECRET = 'hardcoded-secret';
import { JWT_SECRET } from './secrets';
jwt.sign(payload, JWT_SECRET); // Value unknown at lint timeMitigation: Apply the rule to all files. Use secret scanning tools across the entire repository.
Object Property Secrets
Why: When identifier references are treated as safe (could be config), deeply nested literals are missed.
// ❌ NOT DETECTED - Nested in object created elsewhere
const config = createConfig(); // Returns { secret: 'hardcoded' }
jwt.sign(payload, config.secret); // Member expression treated as safeMitigation: Keep secrets in environment variables or a secret manager, and read them where the key is used.
Reassignable Bindings
Why: the key is read through a same-file const (including const SECRET = process.env.JWT_SECRET || 'secret'), but a let / var can be reassigned, so its initial value proves nothing.
// ⚠️ NOT DETECTED - a let can be reassigned
let key = 'my-secret-key';
jwt.sign(payload, key);Mitigation: Use const bindings and avoid variable reassignment for secrets.
Further Reading
⚙️ Options
| Option | Type | Default | Description |
|---|---|---|---|
envPatterns | string[] | [] | No effect — accepted so existing configs stay valid; the rule does not read it yet. |
trustedSanitizers | string[] | [] | No effect — accepted so existing configs stay valid; the rule does not read it yet. |
trustedAnnotations | string[] | [] | No effect — accepted so existing configs stay valid; the rule does not read it yet. |
strictMode | boolean | false | No effect — accepted so existing configs stay valid; the rule does not read it yet. |
Use this rule
npm install -D oxlint-plugin-jwt-security{ "jsPlugins": ["oxlint-plugin-jwt-security"], "rules": { "jwt-security/no-hardcoded-secret": "error" }}import { defineConfig } from 'vite-plus';export default defineConfig({ lint: { jsPlugins: ['oxlint-plugin-jwt-security'], rules: { 'jwt-security/no-hardcoded-secret': 'error', }, },});oxlint-plugin-jwt-security re-exports eslint-plugin-jwt-security/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.
Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.