Skip to main content
interlace
Plugin: jwtRules

no-weak-secret

The rule provides LLM-optimized error messages (Compact 2-line format) with actionable security guidance:

Require strong secrets (256+ bits) for HMAC-based JWT signing

Severity: 🟠 High
CWE: CWE-326

Error Message Format

The rule provides LLM-optimized error messages (Compact 2-line format) with actionable security guidance:

⚠️ CWE-326 OWASP:A04 CVSS:5.9 | Inadequate Encryption Strength detected | MEDIUM
   Fix: Review and apply the recommended fix | https://owasp.org/Top10/A04_2021/

Message Components

ComponentPurposeExample
Risk StandardsSecurity benchmarksCWE-326 OWASP:A04 CVSS:5.9
Issue DescriptionSpecific vulnerabilityInadequate Encryption Strength detected
Severity & ComplianceImpact assessmentMEDIUM
Fix InstructionActionable remediationFollow the remediation steps below
Technical TruthOfficial referenceOWASP Top 10

Rule Details

This rule detects weak secrets used for HMAC-based JWT signing. Weak secrets can be brute-forced offline, allowing attackers to forge tokens.

Examples

❌ Incorrect

// Known weak patterns
jwt.sign(payload, 'secret');
jwt.sign(payload, 'password');
jwt.sign(payload, 'changeme');

// Short secrets (<32 characters)
jwt.sign(payload, 'shortkey');

// The same literals one const away, behind an env fallback, or in bytes
jwt.sign(payload, process.env.JWT_SECRET || 'secret');
await new SignJWT(claims)
  .setProtectedHeader({ alg: 'HS256' })
  .sign(new TextEncoder().encode('secret'));
expressjwt({ secret: 'shhhhhhared-secret', algorithms: ['HS256'] });

✅ Correct

// Environment variable
jwt.sign(payload, process.env.JWT_SECRET);

// Long secret (32+ characters)
jwt.sign(payload, 'ThisIsAVeryStrongSecretThatIs32Ch+');

// Generated secret
jwt.sign(payload, crypto.randomBytes(32).toString('hex'));

// Test files are skipped; node:crypto Sign objects are not JWT signers
signer.sign(privateKey, 'base64'); // 'base64' is an encoding, not a secret

Options

OptionTypeDefaultDescription
minSecretLengthinteger32Minimum secret length in characters
trustedSanitizersstring[][]No effect — accepted so existing configs stay valid; the rule does not read it yet.
trustedAnnotationsstring[][]No effect — accepted so existing configs stay valid; the rule does not read it yet.
strictModebooleanfalseNo effect — accepted so existing configs stay valid; the rule does not read it yet.
{
  "jwt/no-weak-secret": ["error", {
    "minSecretLength": 32
  }]
}
OptionTypeDefaultDescription
minSecretLengthinteger32Minimum secret length (chars)

Known False Negatives

The following patterns are not detected due to static analysis limitations:

Reassignable Bindings

Why: the secret is read through a same-file const, an || / ?? fallback and a byte wrapper, but a let / var can be reassigned.

const weakSecret = 'abc';
jwt.sign(payload, weakSecret); // ❌ detected through the const

// ⚠️ NOT DETECTED - a let can be reassigned
let secret = 'abc';
jwt.sign(payload, secret);

Mitigation: Use environment variables. Apply TypeScript branded types for validated secrets.

Concatenated Secrets

Why: String operations are not evaluated statically.

// ❌ NOT DETECTED - Concatenated weak parts
const part1 = 'short';
const part2 = 'key';
jwt.sign(payload, part1 + part2); // Still weak, but not detected

Mitigation: Use single, long secret strings. Avoid dynamic secret construction.

Encoded Weak Secrets

Why: Base64/hex encoding can hide a weak underlying secret.

// ❌ NOT DETECTED - Encoded but short underlying value
const encoded = Buffer.from('weak').toString('base64'); // 'd2Vhaw=='
jwt.sign(payload, encoded); // String is long enough but entropy is low

Mitigation: Validate secret entropy at runtime. Use cryptographically random secrets.

Environment Variable Values

Why: The rule trusts environment variables but cannot verify their actual values.

// ❌ NOT DETECTED - Weak secret in env var
// .env: JWT_SECRET=password
jwt.sign(payload, process.env.JWT_SECRET); // Trusts the env var

Mitigation: Use secret rotation policies. Validate secret strength at application startup.

Secrets from Configuration Files

Why: Values loaded from config files are treated as safe.

// ❌ NOT DETECTED - Weak secret in config
import { JWT_SECRET } from './config'; // Config contains 'weak123'
jwt.sign(payload, JWT_SECRET);

Mitigation: Apply secret validation at config load time. Use environment variables for secrets.

Further Reading

Use this rule

npm install -D oxlint-plugin-jwt-security
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-jwt-security"],  "rules": {    "jwt-security/no-weak-secret": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-jwt-security'],    rules: {      'jwt-security/no-weak-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.