Incomplete Comparison with Missing Factors
Description
Incomplete Comparison with Missing Factors occurs when a product performs a comparison between entities but does not include all relevant factors or attributes necessary for a complete and secure comparison. This can lead to incorrect equality or inequality determinations, bypassing security controls. Common examples include comparing only part of a password, checking only a subset of authentication tokens, or validating only some fields of a certificate. The missing factors may seem unimportant but can be crucial for security decisions.
Risk
This vulnerability can lead to authentication bypasses, authorization failures, and data integrity issues. Attackers may craft inputs that satisfy incomplete comparisons while differing in unchecked attributes. Password comparisons that only check the first N characters allow any password with matching prefixes. Certificate validation that skips certain fields may accept fraudulent certificates. Session token comparisons missing entropy checks may be vulnerable to brute force. The severity depends on what security decisions rely on the flawed comparison.
Solution
Ensure all security-relevant attributes are included in comparisons. For passwords, always compare the complete values using constant-time comparison functions. For certificates, validate all relevant fields including issuer, validity period, key usage, and the entire chain. For tokens and identifiers, compare all bytes. Use well-tested comparison functions from security libraries rather than implementing custom comparisons. Document which factors are compared and justify any exclusions. Test comparisons with inputs that differ only in excluded factors.
Common Consequences
| Impact | Details |
|---|---|
| Access Control | Scope: Access Control Bypass Protection Mechanism - Incomplete comparisons may allow unauthorized access when security-critical differences are not checked. |
| Integrity | Scope: Integrity Modify Application Data - Incorrect equality determinations can lead to data corruption or unauthorized modifications. |
| Confidentiality | Scope: Confidentiality Read Application Data - Authentication bypasses from incomplete comparisons may expose sensitive data. |
Example Code
Vulnerable Code
// Vulnerable: Only comparing first N characters of password
#include <string.h>
int vulnerable_check_password(const char *input, const char *stored) {
// Vulnerable: Only checks first 8 characters
// "password123" and "password_anything" would match
return strncmp(input, stored, 8) == 0;
}
// Attacker can use any password starting with same 8 chars
// Vulnerable: Incomplete token comparison
public class VulnerableTokenValidator {
public boolean validateToken(String token) {
// Vulnerable: Only checks prefix and doesn't verify signature
String[] parts = token.split("\\.");
if (parts.length != 3) {
return false;
}
// Only validates header format
String header = parts[0];
if (!header.startsWith("eyJ")) { // Base64 of {"
return false;
}
// Vulnerable: Skips signature verification (parts[2])!
// Attacker can modify payload without detection
return true;
}
}
# Vulnerable: Incomplete certificate validation
import ssl
def vulnerable_verify_certificate(cert):
# Vulnerable: Only checks some fields
# Check issuer
if cert.get_issuer().CN != "Trusted CA":
return False
# Check validity period
if cert.has_expired():
return False
# Vulnerable: Missing checks:
# - Certificate chain validation
# - Key usage constraints
# - Subject Alternative Names
# - Revocation status (CRL/OCSP)
# - Signature verification
return True
// Vulnerable: Incomplete API key comparison
function vulnerableValidateApiKey(providedKey, storedKey) {
// Vulnerable: Only compares length and first few characters
if (providedKey.length !== storedKey.length) {
return false;
}
// Vulnerable: Only checks first 16 characters
const prefix = providedKey.substring(0, 16);
const storedPrefix = storedKey.substring(0, 16);
return prefix === storedPrefix;
// Attacker can use any key with matching length and prefix
}
// Vulnerable: Incomplete session comparison
type Session struct {
ID string
UserID int
CreatedAt time.Time
ExpiresAt time.Time
IPAddress string
}
func vulnerableSessionMatch(s1, s2 *Session) bool {
// Vulnerable: Only compares ID, missing other security factors
return s1.ID == s2.ID
// Should also compare:
// - UserID (prevent session hijacking across users)
// - IPAddress (detect session theft)
// - ExpiresAt (ensure session hasn't expired)
}
// Vulnerable: Incomplete file validation
function vulnerableValidateUpload($file) {
// Vulnerable: Only checks extension
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif'];
if (!in_array(strtolower($extension), $allowedExtensions)) {
return false;
}
// Vulnerable: Missing checks:
// - MIME type verification
// - File magic bytes
// - File content validation
// - File size limits
return true; // Attacker can upload malicious.php.jpg
}
Fixed Code
// Fixed: Complete password comparison
#include <string.h>
#include <openssl/crypto.h>
int fixed_check_password(const char *input, const char *stored) {
size_t input_len = strlen(input);
size_t stored_len = strlen(stored);
// Fixed: Compare entire passwords
// Use constant-time comparison to prevent timing attacks
if (input_len != stored_len) {
// Still do comparison to maintain constant time
CRYPTO_memcmp(input, stored, stored_len);
return 0;
}
// Fixed: Complete comparison of all characters
return CRYPTO_memcmp(input, stored, stored_len) == 0;
}
// Fixed: Complete token validation including signature
import java.security.*;
import java.util.Base64;
public class FixedTokenValidator {
private final PublicKey publicKey;
public FixedTokenValidator(PublicKey publicKey) {
this.publicKey = publicKey;
}
public boolean validateToken(String token) throws Exception {
String[] parts = token.split("\\.");
if (parts.length != 3) {
return false;
}
String header = parts[0];
String payload = parts[1];
String signature = parts[2];
// Fixed: Validate header format
if (!isValidHeader(header)) {
return false;
}
// Fixed: Validate payload claims
if (!isValidPayload(payload)) {
return false;
}
// Fixed: Verify signature (previously missing!)
String signedContent = header + "." + payload;
byte[] signatureBytes = Base64.getUrlDecoder().decode(signature);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
sig.update(signedContent.getBytes("UTF-8"));
if (!sig.verify(signatureBytes)) {
return false; // Signature verification failed
}
return true;
}
private boolean isValidHeader(String header) {
// Validate header content
return header != null && header.startsWith("eyJ");
}
private boolean isValidPayload(String payload) {
// Validate payload claims including expiration
// Implementation details...
return true;
}
}
# Fixed: Complete certificate validation
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.x509.oid import ExtensionOID
import datetime
def fixed_verify_certificate(cert, trusted_ca_cert, hostname):
"""Complete certificate validation with all security factors"""
# Fixed: Check certificate chain
try:
cert.verify_directly_issued_by(trusted_ca_cert)
except Exception as e:
return False, f"Chain verification failed: {e}"
# Fixed: Check validity period
now = datetime.datetime.utcnow()
if now < cert.not_valid_before or now > cert.not_valid_after:
return False, "Certificate not within validity period"
# Fixed: Verify signature
try:
trusted_ca_cert.public_key().verify(
cert.signature,
cert.tbs_certificate_bytes,
cert.signature_algorithm_parameters
)
except Exception:
return False, "Signature verification failed"
# Fixed: Check key usage
try:
key_usage = cert.extensions.get_extension_for_oid(
ExtensionOID.KEY_USAGE
)
if not key_usage.value.digital_signature:
return False, "Certificate not valid for digital signature"
except x509.ExtensionNotFound:
return False, "Missing key usage extension"
# Fixed: Validate Subject Alternative Names for hostname
try:
san = cert.extensions.get_extension_for_oid(
ExtensionOID.SUBJECT_ALTERNATIVE_NAME
)
names = san.value.get_values_for_type(x509.DNSName)
if hostname not in names:
return False, f"Hostname {hostname} not in SAN"
except x509.ExtensionNotFound:
# Fall back to Common Name
cn = cert.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)
if not cn or cn[0].value != hostname:
return False, "Hostname mismatch"
# Fixed: Check revocation status (simplified)
# In production, implement OCSP or CRL checking
return True, "Certificate valid"
// Fixed: Complete API key comparison
const crypto = require('crypto');
function fixedValidateApiKey(providedKey, storedKey) {
// Fixed: Validate input types
if (typeof providedKey !== 'string' || typeof storedKey !== 'string') {
return false;
}
// Fixed: Check complete length
if (providedKey.length !== storedKey.length) {
return false;
}
// Fixed: Use constant-time comparison for ENTIRE key
// Prevents timing attacks and ensures all bytes are compared
return crypto.timingSafeEqual(
Buffer.from(providedKey),
Buffer.from(storedKey)
);
}
// Fixed: With additional validation factors
function fixedValidateApiKeyComplete(request, storedKeyData) {
const providedKey = request.headers['x-api-key'];
// Fixed: Compare all relevant factors
// Factor 1: Key value
if (!crypto.timingSafeEqual(
Buffer.from(providedKey || ''),
Buffer.from(storedKeyData.key)
)) {
return false;
}
// Factor 2: Key not expired
if (new Date() > new Date(storedKeyData.expiresAt)) {
return false;
}
// Factor 3: Key not revoked
if (storedKeyData.revoked) {
return false;
}
// Factor 4: Request within rate limits
if (storedKeyData.requestCount > storedKeyData.rateLimit) {
return false;
}
return true;
}
// Fixed: Complete session comparison
type Session struct {
ID string
UserID int
CreatedAt time.Time
ExpiresAt time.Time
IPAddress string
UserAgent string
}
func fixedSessionMatch(provided, stored *Session) (bool, string) {
// Fixed: Compare ALL security-relevant factors
// Factor 1: Session ID
if !secureCompare(provided.ID, stored.ID) {
return false, "session ID mismatch"
}
// Factor 2: User ID (prevent cross-user hijacking)
if provided.UserID != stored.UserID {
return false, "user ID mismatch"
}
// Factor 3: Session not expired
if time.Now().After(stored.ExpiresAt) {
return false, "session expired"
}
// Factor 4: IP address (detect session theft)
// May want to allow subnet changes for mobile users
if provided.IPAddress != stored.IPAddress {
// Log suspicious activity but may not reject
log.Printf("IP change detected: %s -> %s", stored.IPAddress, provided.IPAddress)
}
// Factor 5: User agent consistency
if provided.UserAgent != stored.UserAgent {
log.Printf("User agent change detected")
}
return true, ""
}
// Constant-time string comparison
func secureCompare(a, b string) bool {
if len(a) != len(b) {
return false
}
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
}
// Fixed: Complete file validation
function fixedValidateUpload($file) {
$errors = [];
// Factor 1: Check extension
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif'];
if (!in_array($extension, $allowedExtensions)) {
$errors[] = "Invalid file extension";
}
// Factor 2: Check MIME type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mimeType, $allowedMimes)) {
$errors[] = "Invalid MIME type: $mimeType";
}
// Factor 3: Verify magic bytes
$handle = fopen($file['tmp_name'], 'rb');
$header = fread($handle, 8);
fclose($handle);
$validHeaders = [
'image/jpeg' => ["\xFF\xD8\xFF"],
'image/png' => ["\x89PNG\r\n\x1a\n"],
'image/gif' => ["GIF87a", "GIF89a"]
];
$headerValid = false;
if (isset($validHeaders[$mimeType])) {
foreach ($validHeaders[$mimeType] as $validHeader) {
if (strpos($header, $validHeader) === 0) {
$headerValid = true;
break;
}
}
}
if (!$headerValid) {
$errors[] = "File header does not match expected format";
}
// Factor 4: Check file size
$maxSize = 5 * 1024 * 1024; // 5MB
if ($file['size'] > $maxSize) {
$errors[] = "File too large";
}
// Factor 5: Verify it's actually an image
$imageInfo = @getimagesize($file['tmp_name']);
if ($imageInfo === false) {
$errors[] = "File is not a valid image";
}
return empty($errors) ? true : $errors;
}
CVE Examples
- CVE-2005-2177: Product only compared first 8 characters of passwords.
- CVE-2002-1798: Product only validated certificate issuer without checking the full chain.
Related CWEs
- CWE-697: Incorrect Comparison (parent)
- CWE-187: Partial String Comparison (child)
- CWE-1024: Comparison of Incompatible Types (sibling)
References
- MITRE Corporation. "CWE-1023: Incomplete Comparison with Missing Factors." https://cwe.mitre.org/data/definitions/1023.html
- OWASP. "Authentication Cheat Sheet."
- NIST. "Digital Identity Guidelines."