Skip to content

CWE-597: Use of Wrong Operator in String Comparison

Overview

Using reference equality (==) instead of value equality (.equals()) for string comparison in Java compares memory addresses, not content, causing security checks to fail unpredictably when strings are dynamically created vs literals, enabling authentication bypass and logic errors. Note: In C#, == is overloaded for strings and performs value comparison, making it safe to use.

Primary Defence: Always use .equals() in Java for string content comparison; use constant-first pattern for null safety; prefer enums over strings for security-critical values like roles and permissions. In C#, both == and .Equals() work for string comparison, though .Equals() with StringComparison options provides more control.

OWASP Classification

A04:2021 - Insecure Design

Risk

High: Wrong string operators cause authentication bypass (password checks), authorization failures (role checks), security check bypass (token validation), and unpredictable behavior since == sometimes works (interned strings) and sometimes doesn't.

Remediation Steps

Core principle: Use correct string comparison semantics (and constant-time for secrets); never rely on reference/identity operators for equality.

Locate Incorrect String Comparisons

When reviewing security scan results:

  • Find == comparisons: Search for string comparisons using == or !=
  • Check security-critical code: Authentication, authorization, token validation
  • Look for password checks: Comparing passwords with ==
  • Find role comparisons: User role checks with ==
  • Check token validation: CSRF tokens, API keys compared with ==

Search patterns:

# Java
grep -rn '== "' --include="*.java"
grep -rn '!= "' --include="*.java"
grep -rn 'password.*==' --include="*.java"
grep -rn 'role.*==' --include="*.java"

# C#
grep -rn '== "' --include="*.cs"
grep -rn 'password.*==' --include="*.cs"

Use .equals() for String Comparison (Primary Defense)

See the Common Vulnerable Patterns and Secure Patterns sections below for detailed examples.

Common Vulnerable Patterns

Reference Equality in Authentication

// VULNERABLE - reference equality (==)
String password = request.getParameter("password");
if (password == "admin123") {  // Compares memory addresses!
    grantAccess();  // Almost NEVER works for user input
}

Why is this vulnerable: The == operator compares memory addresses (reference equality), not string content. String literals like "admin123" are stored in the string pool and interned, but user input from request.getParameter() creates new String objects on the heap with different memory addresses. Even if the user types "admin123", the comparison password == "admin123" will almost always return false because they're different objects, causing authentication to fail. However, in rare cases with string interning optimizations, it might return true unpredictably, making this a timing-dependent security vulnerability.

Reference Equality in Authorization

String role = user.getRole();
if (role == "ADMIN") {  // May work sometimes (string interning)
    allowAdminAction();  // Unreliable and dangerous!
}

Why is this vulnerable: Authorization checks using == create unpredictable behavior. If the role string comes from a database query, it's a new String object and role == "ADMIN" returns false even for actual administrators, denying access. However, if the role comes from a constant or gets interned by the JVM, the comparison might succeed. This makes authorization work for some users but not others, depending on how the string was created - a critical security flaw that's difficult to debug and can bypass access controls unpredictably.

Reference Equality in Token Validation

String token = request.getParameter("csrf");
if (token == session.getAttribute("csrf")) {  // WRONG
    processRequest();  // May bypass CSRF protection!
}

Why is this vulnerable: CSRF protection relies on comparing a token from the request with one stored in the session. Using == compares object references, which will almost always fail since the token from request.getParameter() is a different object than the one from session.getAttribute(). This causes legitimate requests to be rejected. However, if validation logic contains fallback paths or exception handling that processes requests anyway, the failed comparison could lead to bypassing CSRF protection entirely, allowing attackers to perform unauthorized actions.

Secure Patterns

Value Equality for Authentication

// SECURE - value equality (.equals())
String password = request.getParameter("password");
if ("admin123".equals(password)) {  // Compares content
    grantAccess();
}

Why this works: The .equals() method compares string content character-by-character, not memory addresses, ensuring reliable comparison regardless of how the string was created. Placing the constant first ("admin123".equals(password)) provides null safety - if password is null, the method returns false instead of throwing NullPointerException. This works for all strings: literals, user input, database results, or dynamically constructed strings. The Java String class implements .equals() to compare the actual char array content, making it the correct choice for security-critical string comparisons.

Value Equality for Authorization

String role = user.getRole();
if ("ADMIN".equals(role)) {  // Use constant first (null-safe)
    allowAdminAction();
}

Why this works: Using .equals() ensures the authorization check examines the actual role value, not object identity. The constant-first pattern ("ADMIN".equals(role)) prevents NullPointerException if role is null, returning false instead of crashing. This approach works consistently whether the role comes from a database (new String object), a constant (interned), or any other source. The method compares each character, so "ADMIN" matches "ADMIN" regardless of how either string was created, providing reliable authorization checks across the entire application.

Value Equality for Token Validation

String expectedToken = (String) session.getAttribute("csrf");
String actualToken = request.getParameter("csrf");

if (expectedToken != null && expectedToken.equals(actualToken)) {
    processRequest();  // Compares values correctly
}

Why this works: The .equals() method compares the actual token values character-by-character, ensuring CSRF protection works reliably. The explicit null check prevents NullPointerException if the session token doesn't exist. This correctly validates that the token from the request matches the token stored in the session, regardless of how the strings were created or stored. For security-sensitive comparisons like CSRF tokens, consider using constant-time comparison methods like MessageDigest.isEqual() to prevent timing attacks, but .equals() is the minimum requirement for correct functionality.

Null-Safe String Comparison

Java - Constant-First Pattern

// Constant first (null-safe)
if ("admin".equals(username)) {
    grantAdmin();
}
// If username=null, returns false (no exception)

// Explicit null check
if (username != null && username.equals("admin")) {
    grantAdmin();
}

// Java 7+ - Objects utility
import java.util.Objects;

if (Objects.equals(username, "admin")) {
    grantAdmin();  // Handles nulls on both sides
}

// Returns true only if both are equal (handles nulls)
Objects.equals(null, null);  // true
Objects.equals("admin", null);  // false
Objects.equals(null, "admin");  // false
Objects.equals("admin", "admin");  // true

Why this works: The constant-first pattern ensures null safety without explicit null checks. When you call "admin".equals(username), if username is null, the method safely returns false instead of throwing a NullPointerException. The Objects.equals() utility method (Java 7+) provides even more flexibility by handling null on both sides of the comparison, returning true only when both values are equal or both are null. This eliminates a common source of runtime errors while maintaining correct comparison semantics.

C# - String Comparison

// C# overloads == for string value comparison
string role = GetUserRole();

if (role == "Administrator") {
    AllowAdmin();  // Works correctly in C#
}
// Note: Be careful with null - if role is null, this throws NullReferenceException

// Value comparison with null safety
if (role?.Equals("Administrator") == true) {
    AllowAdmin();
}

// Static method (null-safe)
if (string.Equals(role, "Administrator")) {
    AllowAdmin();  // Returns false if role is null
}

// Recommended for null safety and culture control
if (string.Equals(role, "Administrator", StringComparison.Ordinal)) {
    AllowAdmin();  // Returns false if role is null
}

Why this works: In C#, the == operator works correctly for string value comparison due to operator overloading, but it throws NullReferenceException if the left operand is null. The null-conditional operator ?. combined with .Equals() provides safe null handling, returning false if the string is null. The static string.Equals() method is the recommended approach as it handles nulls gracefully and allows explicit culture/case comparison control through StringComparison options, ensuring predictable behavior across different locales.

Case-Insensitive Comparisons

Java - equalsIgnoreCase

// Case-insensitive comparison
String method = request.getMethod();
if ("POST".equalsIgnoreCase(method)) {  // Matches "post", "Post", "POST"
    handlePost();
}

String role = user.getRole();
if ("administrator".equalsIgnoreCase(role)) {  // Matches "ADMINISTRATOR", "Administrator"
    allowAdmin();
}

// Don't use toLowerCase() then equals (less efficient, locale issues)
// LESS EFFICIENT:
if (role.toLowerCase().equals("admin")) { }  // Two operations

// BETTER:
if ("admin".equalsIgnoreCase(role)) { }  // One operation

Why this works: The .equalsIgnoreCase() method performs case-insensitive comparison in a single operation, comparing characters while ignoring case differences. This is more efficient than converting strings to lowercase/uppercase first (which creates new string objects) and avoids locale-specific issues that can arise from case conversion. The constant-first pattern also provides null safety, making it both secure and efficient.

C# - StringComparison.OrdinalIgnoreCase

string role = GetUserRole();

// Case-insensitive with null safety
if (role?.Equals("Administrator", StringComparison.OrdinalIgnoreCase) == true) {
    AllowAdmin();
}

// Recommended approach
if (string.Equals(role, "Administrator", StringComparison.OrdinalIgnoreCase)) {
    AllowAdmin();  // Returns false if role is null
}

Why this works: The StringComparison.OrdinalIgnoreCase option performs a fast, culture-insensitive case-insensitive comparison that's ideal for security-related strings like roles, commands, or identifiers. Using string.Equals() static method ensures null safety, returning false if either string is null rather than throwing an exception. This approach provides predictable behavior regardless of the system's current culture settings.

Using Enums for Type Safety

// BETTER - use enums instead of strings for roles
public enum Role {
    USER, ADMIN, MODERATOR
}

public class User {
    private Role role;  // Type-safe

    public Role getRole() {
        return role;
    }
}

// Safe to use == with enums
if (user.getRole() == Role.ADMIN) {  // OK - enums are singletons
    allowAdminAction();
}

// For HTTP methods
public enum HttpMethod {
    GET, POST, PUT, DELETE, PATCH
}

if (request.getMethod() == HttpMethod.POST) {  // Type-safe
    handlePost();
}

Why this works: Using enums instead of strings provides compile-time type safety and eliminates string comparison issues entirely. Enums are implemented as singleton instances in Java, making reference equality (==) safe and efficient. The compiler enforces valid values, preventing typos and invalid states. This approach is recommended for security-critical values like roles, permissions, and states, as it eliminates the entire class of string comparison vulnerabilities.

Switch Statements

// Switch uses equals() internally
String action = request.getParameter("action");

switch (action) {  // Safe - uses equals()
    case "delete":
        handleDelete();
        break;
    case "update":
        handleUpdate();
        break;
    case "create":
        handleCreate();
        break;
    default:
        handleDefault();
}

Why this works: Java's switch statement internally uses .equals() for String case labels (since Java 7), making it safe for string comparison. The switch statement compares the input string's value against each case label using proper value equality, not reference equality. This provides both readability and correct behavior, though using enums with switch statements is even better for type safety.

Understanding String Interning and Reference Equality

String s1 = "hello";  // String literal - interned in pool
String s2 = "hello";  // Same literal - references same object
System.out.println(s1 == s2);  // true (same memory address)

String s3 = new String("hello");  // New object on heap
System.out.println(s1 == s3);  // false (different addresses)
System.out.println(s1.equals(s3));  // true (same content)

// User input NEVER interned
String s4 = request.getParameter("name");
if (s4 == "admin") {  // NEVER works for user input!
    grantAdmin();  // Never executed
}

Why this happens: The JVM optimizes memory by storing string literals in a special pool called the "string pool" where identical literals share the same memory address (string interning). When you write "hello" twice, both variables point to the same object, so == returns true. However, strings created with new String(), read from databases, or received as user input create new objects with different memory addresses, causing == to return false even when the content is identical. This makes == unreliable for string comparison - it works unpredictably based on how the string was created, not its actual value.

Language-Specific Notes

Java

  • Must use .equals(): The == operator compares references, not values
  • Use Objects.equals() for null-safe comparison
  • Constant-first pattern: "constant".equals(variable) prevents NullPointerException
  • Use .equalsIgnoreCase() for case-insensitive comparison

C#

  • == is safe for strings: The == operator is overloaded to compare string values, not references
  • Null safety: Use ?.Equals() or string.Equals() static method to handle nulls
  • Best practice: Use string.Equals() with StringComparison options for explicit culture/case control
  • Case-insensitive: Use StringComparison.OrdinalIgnoreCase or StringComparison.InvariantCultureIgnoreCase

Security Checklist

  • No == or != used for string comparison in security-critical Java code (C# == is safe for strings)
  • All Java string comparisons use .equals() or Objects.equals()
  • C# string comparisons use ==, .Equals(), or string.Equals() with appropriate null handling
  • Case-insensitive comparisons use .equalsIgnoreCase() (Java) or StringComparison.OrdinalIgnoreCase (C#)
  • Constant-first pattern used for null safety in Java ("constant".equals(variable))
  • C# null safety handled with ?.Equals(), string.Equals(), or null checks before ==
  • Password comparisons use constant-time comparison (MessageDigest.isEqual() in Java)
  • Security-critical comparisons (passwords, roles, tokens) verified to use value equality
  • Enums used instead of strings for roles and permissions where appropriate
  • Tests verify string comparison works with dynamically created strings
  • Code review confirms no reference equality issues in authentication/authorization
  • SAST tools configured to detect == in Java string comparisons

Additional Resources