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
| Impact | Details |
|---|---|
| Access Control | Scope: Access Control Bypass Protection Mechanism - Type confusion in authentication checks may allow unauthorized access. |
| Integrity | Scope: Integrity Modify Application Data - Incorrect comparisons can lead to wrong data being processed or modified. |
| Confidentiality | Scope: 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.
Related CWEs
- 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
- MITRE Corporation. "CWE-1024: Comparison of Incompatible Types." https://cwe.mitre.org/data/definitions/1024.html
- OWASP. "PHP Type Juggling Vulnerabilities."
- PHP Manual. "Comparison Operators."