Skip to content

CWE-597: Use of Wrong Operator in String Comparison

Overview

Languages disagree about what == means for a string, and code written on the wrong assumption compiles cleanly while making the wrong decision about authentication, authorization or a token. There are three failure modes, and the fix differs for each:

  • Identity where content was meant. In Java, == asks whether two references point at the same object, so a value that arrived from a request or a database never matches a literal.
  • Coercion where exact matching was meant. In PHP, == converts operands to a common type first, so two different strings that both look numeric compare equal.
  • A locale-dependent transformation before the comparison. In C# and Java alike, folding case with a culture-aware ToLower()/toLowerCase() makes the result depend on which locale the process is running under.

None of the three is reliably "fail open" or "fail closed" on its own. Which way one fails depends on whether the broken operator matches too little or too much and on whether a match admits or refuses; the table under Common Vulnerable Patterns crosses the two, because taking either alone gets the direction backwards.

Primary Defence: Use your language's value-equality API for ordinary security decisions such as roles and permission names. Use a password verifier for passwords and a constant-time comparison for tokens, signatures and API keys; never rely on identity/reference operators or loosely-typed comparison operators for these checks, and never fold case with a locale-dependent call before comparing.

Relationship to Other CWEs

Both of this page's MITRE parents are absent here, and MITRE relates CWE-597 to nothing this corpus covers. The bullets below are the pages a reader holding this finding would actually need.

  • CWE-597 (this page) - a string compared with an operator that does not mean value equality in the language being used, so the comparison decides on identity, on a coerced type, or on a locale-folded form
  • CWE-595 (Comparison of Object References Instead of Object Contents) and CWE-480 (Use of Incorrect Operator) - this page's two MITRE parents, the first for the identity case specifically and the second for wrong operators generally. Neither has a page here
  • CWE-183 (Permissive List of Allowed Inputs) - the neighbouring way a comparison admits more than intended. Both end in a check that matches too much; CWE-183 is a pattern that is too broad, this page is an operator that does not mean what the author thought
  • CWE-185 (Incorrect Regular Expression) - the same failure where a regex rather than an operator is doing the comparing
  • CWE-208 (Observable Timing Discrepancy) - the reason this page's fix is not simply "use value equality" everywhere. Value equality is right for roles and permission names; tokens, signatures and API keys need a constant-time comparison, and reaching for equals() there fixes CWE-597 and leaves CWE-208

OWASP Classification

A06:2025 - Insecure Design

Risk

High: A wrong string comparison operator makes an authentication, authorization or CSRF/token check decide the wrong way. The failure can be intermittent - working for some inputs and failing for others depending on how the strings were constructed - which makes it easy to miss in testing.

Severity varies more within this CWE than between languages, and two of the four possible outcomes are not breaches at all: one is an outage, the other silently blocks legitimate users. All four produce the same scanner result and the same one-word fix, so rate a finding only after answering two questions - whether the broken operator matches too little or too much, and whether a match admits or refuses. The table under Common Vulnerable Patterns crosses them and says which outcome is which.

Remediation Steps

Core Principle: Use your language's value-equality comparison for strings, and a constant-time comparison for secrets.

Trace the Data Path

  • Source: A string value obtained from user input, a database, a session, or a parsed token
  • Sink: A comparison used to make a security decision (authentication, authorization, CSRF/token validation)
  • Data Flow / Missing Controls: The comparison uses an operator that does not perform reliable value equality for the language in use

Use Value Equality, Not Identity or Loose Comparison (Primary Defense)

  • In languages where == performs reference/identity comparison on strings (Java), always use the language's value-equality method instead
  • In dynamically or loosely typed languages, use a strict/type-safe comparison operator so type coercion cannot make unrelated values compare equal
  • Where the language offers a null-safe equality utility, use it instead of manual null checks

Do Not Fold Case with a Locale-Dependent Call

  • Where the comparison is meant to ignore case, use the comparison API's own case-insensitive form rather than transforming both sides first - equalsIgnoreCase, StringComparison.OrdinalIgnoreCase, strcasecmp
  • A bare ToLower()/toLowerCase() uses the ambient locale, and in Turkish and Azeri I folds to a dotless ı, so a check written against an ASCII literal stops matching. Where the value must genuinely be transformed rather than compared, name the invariant locale explicitly
  • Establish whether the caller can choose the locale. Some web stacks select it from the request's Accept-Language header, which turns an environment-dependent bug into a remotely triggerable one

Use Constant-Time Comparison for Secrets

  • CSRF tokens, API keys, and HMAC/signature values must be compared with a constant-time function, not a short-circuiting equality operator. Passwords should go through the platform's password-verification API rather than any hand-written comparison
  • A short-circuiting comparison exits at the first difference, so its duration reflects how much of the submitted value matched. What that leaks in practice is coarser than the usual description - most built-in comparisons work a machine word or a vector at a time rather than a character at a time - and the length check leaks cleanly regardless. CWE-208 has the measurements and the per-language contracts; the fix costs one function call either way

Prefer Enums or Typed Values Over Strings (Defense in Depth)

  • For fixed sets like roles and permissions, use an enum or equivalent typed value instead of a raw string - this removes the comparison-operator question entirely and adds compile-time typo protection

Considerations

  • The reported line is a sample. A codebase that compares strings with the wrong operator in one security check usually does it in several, and the language's own linter finds them faster than a reviewer does. Fix the population, then re-run the rule to confirm the only remaining hits are prose or tests
  • Not every finding is a defect. In C# the operator is already a value comparison for two string-typed operands, so the finding is only real if one of the static types is not string. Recording a false positive with the types written down is a legitimate outcome and faster than a rewrite that changes nothing
  • Content equality is the whole fix for a role and half of it for a secret. Correcting the operator on a token, key or signature comparison leaves the timing question open, and correcting it on a password hash leaves the question of whether a bare digest should have been used at all

Test with Malicious Inputs

  • Supply values of an unexpected type (for example, an integer where a string is expected) to confirm loose-comparison bypasses are not possible
  • Submit a value the check is supposed to reject and assert the specific rejection, not merely that the request did not succeed - this is the only assertion that can see a denylist whose comparison never matches
  • Supply dynamically constructed strings that are equal in content but different in origin (read from a database vs. a literal) to confirm the comparison still succeeds
  • Repeat one case-insensitive comparison under a Turkish locale and assert the outcome is unchanged
  • Re-scan with the security scanner to confirm the finding is resolved

Common Vulnerable Patterns

// VULNERABLE - pseudo-code
role = load_role_for(current_user)
if role == "ADMIN":              // identity/reference or loosely-typed comparison
    grant_access()
// Attack: a dynamically constructed value with equal content but different
// identity fails the check (denies legitimate users), or a value of a
// different type coerces to match (grants an attacker access)

Why this is vulnerable: in a statically typed language the operator's meaning is settled by the static types of its operands, not by what they contain. The same line can compare content today and identity tomorrow because a refactor changed a declaration from a string type to a general object type, or because the value now arrives from a generic API that returns the base type. Nothing at the comparison records which of the two it is doing, and the compiler's diagnostic is inconsistent. It may warn where one side is still a literal, then say nothing once the value has been widened on both sides or passed through a generic helper. In a loosely typed language the meaning is settled instead by the runtime types of the values, so the same line can compare exactly for one request and coerce for the next.

Which way a broken comparison fails is settled by two independent things, and reading only one of them gets it backwards. The first is whether the operator under-matches or over-matches. An identity comparison under-matches: it answers false for values that are equal. A coercing comparison over-matches: it answers true for values that are not - measured across 256 string pairs on PHP 8.5.8, == is true everywhere === is, plus 28 pairs where === is false, and there is no pair where it is the other way round. The second is which branch is the permissive one - whether a match admits or refuses.

Crossing those gives four outcomes, and only two of them are breaches:

Allowlist (match admits) Denylist (match refuses)
Under-matches (identity) Denies everyone - an outage, caught in development Refuses nobody - the restriction silently does not exist
Over-matches (coercion) Admits values it should not - the classic bypass Refuses too much - blocks legitimate traffic

So "a denylist fails open" is true of the identity bug and false of the coercion bug, where the denylist is the safe corner and the allowlist is the dangerous one. Work out which kind of operator you are looking at before deciding which checks to worry about. The two harmless corners are still defects worth fixing, but they are not the ones an attacker reaches for.

Secure Patterns

// SECURE - non-secret values: the language's value-equality API
role = load_role_for(current_user)
if value_equals(role, "ADMIN"):
    grant_access()

// SECURE - tokens, API keys, signatures: constant-time comparison
if constant_time_equals(expected_token, submitted_token):
    process_request()

// SECURE - passwords: no comparison of your own at all
if password_verifier.verify(submitted_password, stored_hash):
    grant_access()

Why this works: Value-equality APIs compare content rather than identity or type-coerced representations, so the result depends only on what the strings contain. That is the whole fix for a role, a permission name or an action: values that are not secret, where the only question is whether the operator compares content.

The other two cases need something stronger. A token, key or signature is a secret being compared against a submitted value, so value equality is necessary and not sufficient: a comparison that exits at the first difference reflects how much of the submitted value matched, which is what the constant-time primitive removes. A password should not reach a comparison at all. Every ecosystem ships a verifier - password_verify(), PasswordEncoder.matches(), bcrypt.compare() - which reads the algorithm, cost and salt out of the stored hash, derives the candidate with the same parameters, and compares the results in constant time. Hashing the submitted password yourself and comparing the digests is a different weakness (CWE-916) whichever operator does the comparing.

Language-Specific Guidance

  • C# - Why == on two strings is already an ordinal value comparison, what object and generic parameters change, and which neighbouring APIs are culture-sensitive when the equality ones are not
  • Java - Why == compares references not values, .equals()/Objects.equals(), the constant-first pattern, and why interning makes a green unit test meaningless
  • PHP - Loose (==) vs. strict (===) comparison, the 0e magic-hash collision, and the constructs that compare loosely without an operator in sight

Additional Resources