Comparison of Incompatible Types

Description

Comparison of Incompatible Types occurs when a product performs a comparison between two entities of different types, where the comparison may produce unexpected or incorrect results due to type coercion, implicit conversion, or fundamental incompatibility between the types. This is especially prevalent in dynamically-typed languages where implicit type conversions happen automatically. The comparison may succeed syntactically but fail to produce the intended security or logical outcome, leading to authentication bypasses, authorization failures, or logic errors.

Risk

This vulnerability can cause security controls to fail silently. In PHP, comparing a string to an integer with "==" may yield unexpected true results (e.g., "0e123" == 0 evaluates to true). JavaScript's loose equality can match strings to numbers or null to undefined. Attackers can craft inputs that exploit type coercion to bypass authentication checks, SQL injection filters, or access controls. The risk is high because the code appears correct and may pass testing with expected input types but fails with carefully crafted malicious inputs.

Solution

Use strict type comparison operators where available (=== in PHP/JavaScript, Object.equals() in Java). Validate and sanitize input types before comparison. Convert values to expected types explicitly before comparing. Use static type checking or strict mode in languages that support it. Avoid comparing user input directly against stored values without type normalization. In dynamically-typed languages, add explicit type checks before security-critical comparisons. Consider using strongly-typed languages for security-critical components.

Common Consequences

ImpactDetails
Access ControlScope: Access Control

Bypass Protection Mechanism - Type confusion in authentication checks may allow unauthorized access.
IntegrityScope: Integrity

Modify Application Data - Incorrect comparisons can lead to wrong data being processed or modified.
ConfidentialityScope: Confidentiality

Read Application Data - Authorization bypasses from type confusion may expose sensitive data.

Example Code

Vulnerable Code

<?php
// Vulnerable: Loose comparison with type coercion
function vulnerableLogin($username, $password) {
    $stored_hash = getUserHash($username);

    // Vulnerable: Using == instead of ===
    // If $password is "0" and hash starts with "0e" (scientific notation)
    // PHP treats both as numbers: 0 == 0 -> true!
    if ($password == $stored_hash) {
        return true;  // Authentication bypass!
    }

    return false;
}

// Example: hash "0e462097431906509019562988736854" == "0" is TRUE
// Because PHP converts "0e..." to 0 (scientific notation)
<?php
// Vulnerable: Array vs string comparison
function vulnerableCheckAdmin($role) {
    // Vulnerable: Comparing potentially different types
    if ($role == 'admin') {
        grantAdminAccess();
    }
}

// Attack: Pass role as array: role[]=admin
// In PHP: ['admin'] == 'admin' has unexpected behavior
// Prior to PHP 8: array compared to string was always "greater"
// Vulnerable: JavaScript loose equality
function vulnerableAuthenticate(token, storedToken) {
    // Vulnerable: Using == instead of ===
    if (token == storedToken) {
        return true;
    }
    return false;
}

// Exploits:
// null == undefined -> true
// "0" == 0 -> true
// "" == 0 -> true
// "1" == true -> true
// [] == false -> true (when [] is converted)
// Vulnerable: Comparing different types from JSON
function vulnerableCheckAccess(userId, resourceOwnerId) {
    // JSON may have strings, URL params may be strings
    // But database IDs might be numbers

    // Vulnerable: Types may differ
    if (userId == resourceOwnerId) {
        return true;  // "1" == 1 is true, but what about "1 " == 1?
    }
    return false;
}
# Vulnerable: Comparing bytes and strings in Python 3
def vulnerable_verify_signature(provided, expected):
    # Vulnerable: One might be bytes, other string
    # In Python 3: b"abc" == "abc" is False (no type coercion)
    # But this silent failure might not be detected
    return provided == expected

# If provided is bytes and expected is string, always False
# May cause denial of service or unexpected rejections
// Vulnerable: Integer vs Long comparison in Java
public class VulnerableComparison {

    public boolean vulnerableCheckId(Object providedId, Long storedId) {
        // Vulnerable: May compare Integer to Long
        // Integer and Long are different types

        // Using == compares object references, not values!
        if (providedId == storedId) {
            return true;
        }

        // This is also vulnerable if providedId is Integer
        return providedId.equals(storedId);
        // Integer.equals(Long) always returns false!
    }
}
# Vulnerable: Ruby type coercion in comparisons
def vulnerable_check_amount(user_input, limit)
  # Vulnerable: User input might be string
  # "100" > 50 -> comparing string to integer

  if user_input > limit
    reject_transaction
  else
    process_transaction  # "100" (string) may pass incorrectly
  end
end

Fixed Code

<?php
// Fixed: Strict comparison with type validation
function fixedLogin($username, $password) {
    // Fixed: Validate input types
    if (!is_string($username) || !is_string($password)) {
        return false;
    }

    $stored_hash = getUserHash($username);

    if (!is_string($stored_hash)) {
        return false;
    }

    // Fixed: Use password_verify for proper password checking
    // This handles timing attacks and type issues
    return password_verify($password, $stored_hash);
}

// For other comparisons, use === (strict equality)
function fixedCompareTokens($provided, $stored) {
    // Fixed: Type check first
    if (!is_string($provided) || !is_string($stored)) {
        return false;
    }

    // Fixed: Use strict comparison
    // And constant-time comparison for security
    return hash_equals($stored, $provided);
}
<?php
// Fixed: Type-safe role checking
function fixedCheckAdmin($role) {
    // Fixed: Validate type first
    if (!is_string($role)) {
        error_log("Invalid role type: " . gettype($role));
        return false;
    }

    // Fixed: Use strict comparison
    if ($role === 'admin') {
        grantAdminAccess();
    }
}

// Better: Use enums or constants
class UserRole {
    const ADMIN = 'admin';
    const USER = 'user';
    const GUEST = 'guest';

    public static function isValid($role) {
        return in_array($role, [self::ADMIN, self::USER, self::GUEST], true);
    }
}

function fixedCheckAdminWithEnum($role) {
    if (!is_string($role) || !UserRole::isValid($role)) {
        return false;
    }

    return $role === UserRole::ADMIN;
}
// Fixed: JavaScript strict equality
function fixedAuthenticate(token, storedToken) {
    // Fixed: Type validation
    if (typeof token !== 'string' || typeof storedToken !== 'string') {
        return false;
    }

    // Fixed: Use strict equality ===
    if (token === storedToken) {
        return true;
    }
    return false;
}

// Fixed: With constant-time comparison for security
const crypto = require('crypto');

function fixedAuthenticateSecure(token, storedToken) {
    // Fixed: Type validation
    if (typeof token !== 'string' || typeof storedToken !== 'string') {
        return false;
    }

    // Fixed: Length check first
    if (token.length !== storedToken.length) {
        return false;
    }

    // Fixed: Constant-time comparison
    return crypto.timingSafeEqual(
        Buffer.from(token),
        Buffer.from(storedToken)
    );
}
// Fixed: Type-safe ID comparison
function fixedCheckAccess(userId, resourceOwnerId) {
    // Fixed: Normalize types explicitly
    const normalizedUserId = String(userId).trim();
    const normalizedOwnerId = String(resourceOwnerId).trim();

    // Fixed: Strict comparison after normalization
    return normalizedUserId === normalizedOwnerId;
}

// Alternative: Convert to numbers if IDs are numeric
function fixedCheckAccessNumeric(userId, resourceOwnerId) {
    const numUserId = parseInt(userId, 10);
    const numOwnerId = parseInt(resourceOwnerId, 10);

    // Fixed: Validate conversion succeeded
    if (isNaN(numUserId) || isNaN(numOwnerId)) {
        return false;
    }

    return numUserId === numOwnerId;
}
# Fixed: Explicit type handling in Python
def fixed_verify_signature(provided, expected):
    # Fixed: Normalize types
    if isinstance(provided, str):
        provided = provided.encode('utf-8')
    if isinstance(expected, str):
        expected = expected.encode('utf-8')

    # Fixed: Validate both are bytes now
    if not isinstance(provided, bytes) or not isinstance(expected, bytes):
        raise TypeError("Both values must be strings or bytes")

    # Fixed: Use constant-time comparison
    import hmac
    return hmac.compare_digest(provided, expected)


# With type hints for clarity
from typing import Union
import hmac

def fixed_verify_signature_typed(
    provided: Union[str, bytes],
    expected: Union[str, bytes]
) -> bool:
    """Verify signature with proper type handling."""

    # Normalize to bytes
    if isinstance(provided, str):
        provided = provided.encode('utf-8')
    if isinstance(expected, str):
        expected = expected.encode('utf-8')

    return hmac.compare_digest(provided, expected)
// Fixed: Type-safe comparison in Java
public class FixedComparison {

    public boolean fixedCheckId(Object providedId, Long storedId) {
        if (providedId == null || storedId == null) {
            return false;
        }

        // Fixed: Convert to same type before comparison
        Long providedLong;

        if (providedId instanceof Long) {
            providedLong = (Long) providedId;
        } else if (providedId instanceof Integer) {
            providedLong = ((Integer) providedId).longValue();
        } else if (providedId instanceof String) {
            try {
                providedLong = Long.parseLong((String) providedId);
            } catch (NumberFormatException e) {
                return false;
            }
        } else {
            return false;  // Unsupported type
        }

        // Fixed: Now comparing same types
        return providedLong.equals(storedId);
    }

    // Better: Use generics and require same type
    public <T extends Comparable<T>> boolean safeCompare(T a, T b) {
        if (a == null || b == null) {
            return a == b;  // Both null = equal
        }
        return a.compareTo(b) == 0;
    }
}
# Fixed: Type-safe comparison in Ruby
def fixed_check_amount(user_input, limit)
  # Fixed: Explicit type conversion
  begin
    amount = Float(user_input)
  rescue ArgumentError, TypeError
    raise ArgumentError, "Invalid amount format"
  end

  # Fixed: Ensure limit is also numeric
  limit = Float(limit) unless limit.is_a?(Numeric)

  # Now comparing same types
  if amount > limit
    reject_transaction
  else
    process_transaction
  end
end

# Alternative: Use strong typing with type checking
def fixed_check_amount_strict(user_input, limit)
  # Validate types upfront
  unless user_input.is_a?(Numeric) || user_input.is_a?(String)
    raise TypeError, "Amount must be numeric or string"
  end

  unless limit.is_a?(Numeric)
    raise TypeError, "Limit must be numeric"
  end

  amount = user_input.to_f

  amount > limit ? reject_transaction : process_transaction
end
// Fixed: TypeScript with strict type checking
function fixedCompareValues(a: unknown, b: unknown): boolean {
    // Fixed: Explicit type narrowing
    if (typeof a === 'string' && typeof b === 'string') {
        return a === b;
    }

    if (typeof a === 'number' && typeof b === 'number') {
        return a === b;
    }

    // Convert both to strings for comparison if mixed
    if ((typeof a === 'string' || typeof a === 'number') &&
        (typeof b === 'string' || typeof b === 'number')) {
        return String(a) === String(b);
    }

    // Types are incompatible
    return false;
}

// With strict typing
interface UserId {
    readonly value: string;
}

function createUserId(value: string | number): UserId {
    return { value: String(value) };
}

function compareUserIds(a: UserId, b: UserId): boolean {
    return a.value === b.value;
}

CVE Examples

  • CVE-2015-8562: Type juggling in PHP led to authentication bypass.
  • CVE-2014-0166: Integer comparison issues in WordPress.

  • CWE-697: Incorrect Comparison (parent)
  • CWE-1023: Incomplete Comparison with Missing Factors (sibling)
  • CWE-843: Access of Resource Using Incompatible Type ('Type Confusion') (related)

References

  1. MITRE Corporation. "CWE-1024: Comparison of Incompatible Types." https://cwe.mitre.org/data/definitions/1024.html
  2. OWASP. "PHP Type Juggling Vulnerabilities."
  3. PHP Manual. "Comparison Operators."