VNX-354: Integrity check value validated so that a missing, partial or truncated value passes
Reports signature, MAC and checksum comparisons that can be passed without the right value: verification that runs only when the value is present (if sig and not compare_digest(...)), prefix/suffix/substring tests against a computed digest, and digests truncated before comparison, in Python, JavaScript/TypeScript and Java.
| Field | Value |
|---|---|
| Rule ID | VNX-354 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | Java, JavaScript / TypeScript, Python |
| CWE | CWE-354 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:M/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C |
| Tags | integrity-validation signature hmac checksum cwe-354 |
| Source | internal/sast/rules/vnx-354.rego |
Overview
VNX-354 maps to CWE-354: Improper Validation of Integrity Check Value. It reports, in Python, JavaScript/TypeScript and Java:
- Verification that only runs when the value was sent.
if sig and not hmac.compare_digest(...),if checksum and actual != checksum,if (sig && sig !== expected),if (signature.length && !crypto.timingSafeEqual(...)),if (signature != null && !MessageDigest.isEqual(...)), where the tested name is a signature, MAC, HMAC, digest, hash, checksum or integrity value. A request that leaves the header out skips the check. Not reported when one of the eight lines above rejects a missing value. - A prefix, suffix or substring test against a computed digest (assigned from
hexdigest(),digest(),hmac.new,createHmac,createHash,Mac.doFinalorMessageDigestwithin the 20 lines above, or computed inline):computed.startswith(provided),digest.endsWith(x),x in digest,digest.includes(x). An empty or short value passes. - A digest truncated before it is compared:
hexdigest()[:8] == tag,hash.slice(0, 8) === x,hash.substring(0, 8).equals(x).
Not reported: the positive form (if sig and compare_digest(...): accept), prefix tests against a literal (sig.startswith("sha256=")), collection lookups (digest in known_hashes, hashes.contains(hash)), a truncated digest used as an id and not compared, location.hash, comments and string literals. A non-constant-time comparison is VNX-CRYPTO-008; a verification result that is thrown away is VNX-227.
Why This Matters
A webhook, download or token check is only as strong as its failure path. If the comparison is skipped when the header is missing, or accepts any prefix of the right value, an attacker sends no signature (or an empty one) and the payload is processed as authentic.
What Gets Flagged
# FLAGGED
signature = request.headers.get("X-Hub-Signature-256")
if signature and not hmac.compare_digest(signature, expected):
abort(401)
computed = hmac.new(key, payload, hashlib.sha256).hexdigest()
return computed.startswith(provided)
// FLAGGED
if (sig && sig !== expected) { throw new Error('bad signature'); }
return hash.slice(0, 8) === provided;
# NOT FLAGGED: a missing signature is rejected first
if not signature:
abort(401)
if signature and not hmac.compare_digest(signature, expected):
abort(401)
Remediation
- Reject a request whose signature or checksum is missing before comparing anything.
- Compare the whole value, in constant time:
hmac.compare_digest,crypto.timingSafeEqual(after checking lengths),MessageDigest.isEqual. - Never validate an integrity value with
startswith,in,includesor a truncated slice.
References
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-354
// nosec vnx-354
Or record an ignore rule: vulnetix ignore add --rule vnx-354 --reason "...". See Suppressing findings.