Skip to main content
interlace
Plugin: node-securityRules

require-aead-tag-verification

Require AEAD decryption to verify the authentication tag (setAuthTag + final)

Detects crypto.createDecipheriv() with an AEAD mode (GCM, CCM, OCB, ChaCha20-Poly1305) whose authentication tag is never actually verified. This rule is part of eslint-plugin-node-security and provides LLM-optimized error messages.

🚨 Security rule | ⚠️ Set to error in recommended

Quick Summary

AspectDetails
CWE ReferenceCWE-327 (Broken Crypto)
SeverityHigh (security vulnerability)
Auto-Fix❌ Manual — the tag has to come from somewhere
CategorySecurity
ESLint MCP✅ Optimized for ESLint MCP integration
Best ForNode.js services that decrypt attacker-reachable ciphertext

Vulnerability and Risk

Vulnerability: An AEAD mode is only authenticated when both halves of the contract run:

  1. decipher.setAuthTag(tag) hands the tag to the cipher, and
  2. decipher.final() is what compares it and throws on a mismatch.

Skip either one and Node returns whatever update() produced — attacker-chosen plaintext — with no error at all.

Risk: The code looks authenticated. The algorithm string says aes-256-gcm, so every rule that checks algorithm names passes it. But an attacker who can flip ciphertext bits gets a corresponding plaintext change accepted as authentic: forged sessions, tampered tokens, bit-flipped amounts.

Rule Details

The rule anchors on a const/let binding initialised from createDecipheriv() with a literal AEAD algorithm, then reads every method invoked on that binding:

  • no setAuthTag anywhere → missingAuthTag
  • setAuthTag present, no finalmissingFinal

It deliberately says nothing when it cannot see the whole picture:

  • the decipher is driven as a stream (pipe, write, end, setEncoding) — Node's _flush runs the tag check and emits 'error', so there is no explicit final() to demand;
  • the decipher escapes — passed to pipeline(), returned, stored on an object, reached through a computed key;
  • the algorithm is computed — that is no-dynamic-algorithm-selection's finding, not this one's.

Examples

❌ Incorrect

// No setAuthTag — forged ciphertext decrypts "successfully"
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
let out = decipher.update(ciphertext, 'hex', 'utf8');
out += decipher.final('utf8');

// Tag loaded and then ignored — final() is what verifies it
const d = crypto.createDecipheriv('aes-256-gcm', key, iv);
d.setAuthTag(authTag);
return d.update(ciphertext, 'hex', 'utf8');

✅ Correct

// setAuthTag + final: final() throws if the tag does not verify
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
let out = decipher.update(ciphertext, 'hex', 'utf8');
out += decipher.final('utf8');

// Streamed: end-of-stream runs the same check and emits 'error'
const stream = crypto.createDecipheriv('aes-256-gcm', key, iv);
stream.setAuthTag(authTag);
input.pipe(stream).pipe(output);

Configuration

OptionTypeDefaultDescription
allowInTestsbooleanfalseAllow unverified AEAD decryption in test files
{
  rules: {
    'node-security/require-aead-tag-verification': ['error', {
      allowInTests: false
    }]
  }
}

Security Impact

VulnerabilityCWEOWASPCVSSImpact
Broken/Risky Crypto327A02:20217.5 HighForged ciphertext accepted as authentic
Improper Verification347A02:20217.5 HighIntegrity guarantee silently absent

Known False Negatives

Chained and escaping deciphers

Why: The rule needs a named binding it can follow to every use. A chained call has no binding, and a decipher handed to another function is verified (or not) somewhere this rule cannot see.

// ❌ NOT DETECTED
return crypto.createDecipheriv('aes-256-gcm', key, iv).update(ct);

// ❌ NOT DETECTED — decryptWith() may or may not verify
decryptWith(crypto.createDecipheriv('aes-256-gcm', key, iv));

Mitigation: Bind the decipher to a local and drive it in one place.

Further Reading

⚙️ Options

OptionTypeDefaultDescription
allowInTestsbooleanfalseAllow unverified AEAD decryption in test files

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.