VNX-227: Improper Fulfillment of API Contract: security verification result ignored
A password check, MAC or constant-time comparison, signature verification or OpenSSL verify/RNG call is made as a bare statement, so its verdict is discarded; also PHP openssl_verify() used directly as an if condition.
| Field | Value |
|---|---|
| Rule ID | VNX-227 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | C, C++, Go, Java, JavaScript / TypeScript, PHP, Python, Ruby |
| CWE | CWE-227 |
| CAPEC | CAPEC-97 |
| ATT&CK | T1557 |
| 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 | api-contract unchecked-return authentication signature-verification cwe-227 |
| Source | internal/sast/rules/vnx-227.rego |
Overview
Security APIs such as hmac.compare_digest(), password_verify() or SSL_get_verify_result() do not stop the program when a check fails: they return the verdict, and the contract is that the caller acts on it. VNX-227 reports a call to one of these APIs written as a statement on its own, whose verdict is therefore never read. This maps to CWE-227: Improper Fulfillment of API Contract (‘API Abuse’).
APIs covered: Go hmac.Equal, subtle.ConstantTimeCompare, bcrypt.CompareHashAndPassword, rsa.VerifyPKCS1v15/VerifyPSS, ecdsa.Verify/VerifyASN1, ed25519.Verify; Python compare_digest, bcrypt.checkpw, check_password_hash, check_password, passlib verify; Node.js crypto.timingSafeEqual, crypto.verify, bcrypt.compare/compareSync, argon2.verify, verify() on a crypto.createVerify() object; PHP password_verify, hash_equals, openssl_verify, sodium verify functions; Ruby secure_compare, fixed_length_secure_compare, authenticate; Java MessageDigest.isEqual, BCrypt.checkpw, PasswordEncoder.matches, Signature.verify; C/C++ SSL_get_verify_result, X509_verify_cert, RAND_bytes, RAND_priv_bytes, EVP_DigestVerify(Final), EVP_VerifyFinal, RSA/ECDSA/DSA_verify, CRYPTO_memcmp. Go’s _ = and C’s (void) are reported too: discarding a verdict verifies nothing.
PHP’s openssl_verify() returns 1, 0 or -1 on error; if (openssl_verify(...)) treats the error as a valid signature and is reported as well.
Why This Matters
A discarded verdict turns an authentication or integrity check into a no-op: any password logs in, any webhook signature is accepted, any certificate is trusted.
Not reported: a result that is assigned, returned, tested or part of an expression (also across line breaks), the last expression of a Ruby method or block (its return value), a call that hands the verdict to a callback, APIs that raise on failure (jwt.verify, a JWTVerifier, the cryptography package’s verify), comments, string literals and test code.
What Gets Flagged
bcrypt.CompareHashAndPassword(u.Hash, []byte(password))
return newSession(u)
hmac.compare_digest(sig, expected)
return handle(request.json)
if (openssl_verify($data, $sig, $pub, OPENSSL_ALGO_SHA256)) {
SSL_get_verify_result(ssl);
RAND_bytes(nonce, 16);
Remediation
Test the return value (or the error) and stop when verification fails:
if err := bcrypt.CompareHashAndPassword(u.Hash, []byte(password)); err != nil {
return nil, err
}
if (openssl_verify($data, $sig, $pub, OPENSSL_ALGO_SHA256) !== 1) {
throw new InvalidSignature();
}
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-227
// nosec vnx-227
Or record an ignore rule: vulnetix ignore add --rule vnx-227 --reason "...". See Suppressing findings.