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

ImpactDetails
Access ControlScope: Access Control

Bypass Protection Mechanism - Incomplete comparisons may allow unauthorized access when security-critical differences are not checked.
IntegrityScope: Integrity

Modify Application Data - Incorrect equality determinations can lead to data corruption or unauthorized modifications.
ConfidentialityScope: 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.

  • CWE-697: Incorrect Comparison (parent)
  • CWE-187: Partial String Comparison (child)
  • CWE-1024: Comparison of Incompatible Types (sibling)

References

  1. MITRE Corporation. "CWE-1023: Incomplete Comparison with Missing Factors." https://cwe.mitre.org/data/definitions/1023.html
  2. OWASP. "Authentication Cheat Sheet."
  3. NIST. "Digital Identity Guidelines."