Skip to main content
interlace
Plugin: jwtRules

no-algorithm-confusion

This rule detects algorithm confusion attacks where symmetric algorithms (HS256, HS384, HS512) are used with asymmetr...

Prevent algorithm confusion attacks using symmetric algorithms with asymmetric keys

🚨 Security rule | 💡 Provides LLM-optimized guidance | ⚠️ Set to error in recommended

Quick Summary

AspectDetails
CWE ReferenceCWE-347
SeverityCritical
Auto-Fix❌ No auto-fix available
CategorySecurity
ESLint MCP✅ Optimized for ESLint MCP integration
Best ForProtecting asymmetric JWT verification from algorithm substitution attacks

Rule Details

This rule detects algorithm confusion attacks where symmetric algorithms (HS256, HS384, HS512) are used with asymmetric keys (public keys). An attacker can use the public key as an HMAC secret to forge tokens.

Examples

❌ Incorrect

// HS256 with a key that IS public, by how it was made - VULNERABLE
const publicKey = createPublicKey(pem); // node:crypto
jwt.verify(token, publicKey, { algorithms: ['HS256'] });

// Any symmetric algorithm with public key material
const spki = await importSPKI(pem, 'RS256'); // jose
await jwtVerify(token, spki, { algorithms: ['HS384'] });
const signingKey = await jwksClient({ jwksUri }).getSigningKey(kid); // jwks-rsa
jwt.verify(token, signingKey.getPublicKey(), { algorithms: ['HS512'] });
jwt.verify(token, readFileSync('/keys/jwt.pub'), { algorithms: ['HS256'] });

// HMAC and asymmetric algorithms in ONE whitelist (CVE-2015-9235's shape).
// Reported whatever the key is called: the attacker picks HS256 and the
// verifier HMACs with the key material it holds for RS256.
jwt.verify(token, cert, { algorithms: ['RS256', 'HS256'] });

✅ Correct

// Asymmetric algorithm with public key - SAFE
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
jwt.verify(token, publicKey, { algorithms: ['ES256'] });

// Symmetric algorithm with shared secret - SAFE
jwt.verify(token, sharedSecret, { algorithms: ['HS256'] });
jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });

// A NAME is not evidence: an HMAC secret called PUBLIC_* is still a secret
jwt.verify(token, process.env.PUBLIC_WIDGET_HMAC_SECRET, {
  algorithms: ['HS256'],
});

A key counts as public only on structural evidence: a PEM PUBLIC KEY / CERTIFICATE string (inline or one constant away), createPublicKey() / new X509Certificate() from node:crypto, jose's importSPKI, importX509, createRemoteJWKSet or createLocalJWKSet, anything a jwks-rsa client returns, or a file read whose path ends in .pub, .crt or .cer. What the variable is called is never consulted.

How the Attack Works

  1. Server uses RS256 (asymmetric) with a public/private key pair
  2. Attacker obtains the public key (often publicly available)
  3. Attacker creates a token with alg: "HS256" signed with the public key as secret
  4. Server accepts HS256 in its algorithm list
  5. Server verifies using the public key as HMAC secret - attack succeeds

Known False Negatives

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

Key Source Detection Limits

Why: A key is public only on structural evidence. A .pem file can hold a private key as easily as a public one, so its extension is not evidence, and an HMAC-only list against such a key is not reported. A whitelist that mixes HS* with RS*/ES*/PS*/EdDSA is reported whatever the key is.

// ❌ NOT DETECTED - a .pem path says nothing about which half of the pair it holds
const asymKey = fs.readFileSync('public.pem');
jwt.verify(token, asymKey, { algorithms: ['HS256'] });

Mitigation: Load public keys with createPublicKey() or jose's importSPKI(), which the rule recognises, or name the file .pub.

Dynamic Algorithm Selection

Why: Algorithm computed at runtime cannot be statically verified against key type.

// ❌ NOT DETECTED - Dynamic algorithm
const algo = getUserPreferredAlgorithm(); // Could return 'HS256'
jwt.verify(token, publicKey, { algorithms: [algo] });

Mitigation: Use TypeScript literal types for algorithms. Validate algorithm-key pairs at runtime.

Cross-Module Key Passing

Why: Keys imported from other modules cannot be classified.

// ❌ NOT DETECTED - Key from another module
import { verificationKey } from './keys';
jwt.verify(token, verificationKey, { algorithms: ['HS256'] }); // Is it public or secret?

Mitigation: Apply rule across all modules. Use type annotations for key types.

Key From Configuration Object

Why: Properties of configuration objects are not analyzed for key type.

// ❌ NOT DETECTED - Key in config
const config = loadConfig();
jwt.verify(token, config.key, { algorithms: ['HS256'] }); // Key type unknown

Mitigation: Validate key-algorithm pairs at runtime. Use separate config fields for asymmetric vs symmetric keys.

Further Reading

⚙️ Options

OptionTypeDefaultDescription
symmetricAlgorithmsstring[]["HS256","HS384","HS512"]Algorithms to flag when used with public keys
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.

Use this rule

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