CWE-597: Use of Wrong Operator in String Comparison - Java
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.
Primary Defence: Always use .equals() for string content comparison; use constant-first pattern for null safety ("constant".equals(variable)); use Objects.equals() for null-safe comparison of two variables; prefer enums over strings for security-critical values like roles and permissions; use MessageDigest.isEqual() for constant-time comparison of passwords and tokens to prevent authentication bypass and timing attacks.
Common Vulnerable Patterns
Reference Equality in Authentication
// VULNERABLE - reference equality (==)
@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password) {
String storedPassword = userService.getPassword(username);
if (password == storedPassword) { // WRONG - compares memory addresses!
return "redirect:/dashboard";
}
return "login?error";
}
// Attack: This almost NEVER works for user input
// password parameter creates new String object
// storedPassword from database creates new String object
// Even if passwords match, == returns false
Why is this vulnerable: The == operator compares memory addresses (reference equality), not string content. User input from @RequestParam creates new String objects, and database queries return new String objects - these will always have different memory addresses even when the content is identical, causing == to return false. Authentication fails for all users, effectively creating a denial of service. However, if code has fallback logic or debugging paths that bypass failed checks, attackers might find ways to authenticate without valid credentials.
Reference Equality in Authorization
// VULNERABLE - role check with ==
@GetMapping("/admin")
public String adminPanel(Principal principal) {
User user = userService.findByUsername(principal.getName());
String role = user.getRole(); // From database - new String object
if (role == "ADMIN") { // May work sometimes (string interning)
return "admin";
}
return "access-denied";
}
// UNPREDICTABLE:
// - If role from database: == fails (different objects)
// - If role from enum.toString(): might work (interned)
// - If role from constant: might work (interned)
// Authorization works or fails based on internal JVM optimizations!
Why is this vulnerable: Authorization checks using == create unpredictable behavior depending on how the string was created. Roles from database queries are new String objects (role == "ADMIN" returns false), but roles from constants might be interned (role == "ADMIN" returns true). This makes authorization work for some users but not others based on code paths, not actual permissions. An administrator might be denied access while a regular user with role "ADMIN" from a different code path gets access - a critical security flaw.
Reference Equality in CSRF Token Validation
// VULNERABLE - token comparison with ==
@PostMapping("/transfer")
public ResponseEntity<?> transfer(@RequestParam String csrf, HttpSession session) {
String sessionToken = (String) session.getAttribute("csrf");
if (csrf == sessionToken) { // WRONG - reference comparison
processTransfer();
return ResponseEntity.ok("Transfer complete");
}
return ResponseEntity.status(403).body("Invalid CSRF token");
}
// What happens:
// - sessionToken from session.getAttribute(): new String object
// - csrf from request.getParameter(): new String object
// - == always returns false (different objects)
// All requests rejected, even with valid tokens
Why is this vulnerable: CSRF protection depends on comparing the token from the request with the token stored in the session. Using == compares object references, which always fail since request.getParameter() and session.getAttribute() return different String objects even when values match. This breaks CSRF protection completely - all requests are rejected. If developers add exception handling or debug modes to "fix" the issue, they might accidentally bypass CSRF validation entirely, allowing attackers to perform unauthorized state-changing operations.
Reference Equality in Spring Security
// VULNERABLE - comparing authorities with ==
@Service
public class AuthorizationService {
public boolean hasPermission(Authentication auth, String requiredPermission) {
for (GrantedAuthority authority : auth.getAuthorities()) {
String permission = authority.getAuthority(); // From database
if (permission == requiredPermission) { // WRONG
return true;
}
}
return false;
}
}
// Usage:
if (authService.hasPermission(auth, "DELETE_USER")) { // May fail
deleteUser();
}
Why is this vulnerable: Spring Security's GrantedAuthority objects return permission strings that are often loaded from databases or configuration files, creating new String objects. Comparing with == against string literals fails even when permissions match. This causes access control failures - users with correct permissions are denied access. The intermittent nature (works with some code paths, fails with others) makes it extremely difficult to debug and may lead developers to disable security checks "temporarily" to fix the issue.
Reference Equality in JWT Claims
// VULNERABLE - JWT claim comparison
public boolean validateToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
String issuer = claims.getIssuer(); // New String from JWT parsing
if (issuer == "https://myapp.com") { // WRONG
return true;
}
return false;
}
// JWT issuer from token parsing: new String object
// Literal "https://myapp.com": interned String
// == always returns false - all tokens rejected
Why is this vulnerable: JWT validation extracts claims as new String objects from the parsed token. Comparing the issuer with == fails because the parsed issuer is a different object than the string literal, even when values match. This invalidates all JWTs, breaking authentication. If the code has error handling that proceeds anyway (logging failures but continuing), attackers could potentially bypass token validation entirely. The unpredictable behavior also makes it difficult to detect during testing if some code paths accidentally intern the string.
Secure Patterns
Value Equality for Authentication
// SECURE - value equality (.equals())
@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password) {
User user = userService.findByUsername(username);
if (user != null && user.getPasswordHash().equals(hashPassword(password))) {
return "redirect:/dashboard";
}
return "login?error";
}
// Better: Use constant-first pattern for null safety
String storedHash = userService.getPasswordHash(username);
if (storedHash != null && storedHash.equals(hashPassword(password))) {
grantAccess();
}
// Best: Use Objects.equals for null safety on both sides
if (Objects.equals(storedHash, hashPassword(password))) {
grantAccess(); // Handles nulls on both sides
}
Why this works: The .equals() method compares string content character-by-character, ensuring reliable comparison regardless of how strings were created. It works for user input (new objects), database results (new objects), or string literals (interned objects). The constant-first pattern (storedHash.equals(password)) provides null safety - if storedHash is null, the comparison returns false instead of throwing NullPointerException. Objects.equals() provides even better null handling, returning true only when both values are non-null and equal, or both are null.
Value Equality for Authorization with Enums
// BEST - use enums instead of strings for roles
public enum Role {
USER, ADMIN, MODERATOR
}
@Entity
public class User {
@Enumerated(EnumType.STRING)
private Role role; // Type-safe, stored as string in DB
public Role getRole() {
return role;
}
}
// Authorization check - safe to use == with enums
@GetMapping("/admin")
public String adminPanel(Principal principal) {
User user = userService.findByUsername(principal.getName());
if (user.getRole() == Role.ADMIN) { // OK - enums are singletons
return "admin";
}
return "access-denied";
}
// Why this works: Enums are singletons - only one ADMIN instance exists
// == comparison is safe and efficient for enums
Why this works: Java enums are implemented as singletons - only one instance of each enum constant exists in the JVM. When you compare user.getRole() == Role.ADMIN, you're comparing object references to the same singleton object, so == is safe and correct. This provides type safety (compiler prevents typos like "AMIN"), prevents null pointer issues with constants, and makes authorization checks both safe and efficient. JPA's @Enumerated(EnumType.STRING) stores enums as strings in the database while maintaining type safety in code.
Constant-First Pattern for Null Safety
// SECURE - constant first prevents NullPointerException
@PostMapping("/validate")
public ResponseEntity<?> validate(@RequestParam String action) {
// WRONG - may throw NullPointerException
// if (action.equals("delete")) { // NPE if action is null
// RIGHT - constant first (null-safe)
if ("delete".equals(action)) { // Returns false if action is null
handleDelete();
return ResponseEntity.ok("Deleted");
}
if ("update".equals(action)) {
handleUpdate();
return ResponseEntity.ok("Updated");
}
return ResponseEntity.badRequest().body("Unknown action");
}
// Why this works:
// - "delete" is never null (string literal)
// - If action is null, "delete".equals(null) returns false (no exception)
// - Clear intent: checking if action matches expected constant
Why this works: Placing the known non-null constant first ("delete".equals(action)) guarantees the .equals() method is called on a non-null object. If action is null, the comparison returns false instead of throwing NullPointerException. This defensive programming pattern is especially important for user input, optional parameters, or values that might be null under error conditions. It makes code more robust and prevents null-related crashes that could bypass security checks.
Objects.equals() for Null-Safe Comparison
// SECURE - Objects.equals handles nulls on both sides
import java.util.Objects;
@Service
public class UserService {
public boolean matchesUsername(User user, String targetUsername) {
// Handles all null scenarios correctly:
// Objects.equals(null, null) => true
// Objects.equals("admin", null) => false
// Objects.equals(null, "admin") => false
// Objects.equals("admin", "admin") => true
return Objects.equals(user.getUsername(), targetUsername);
}
public boolean isSameUser(User user1, User user2) {
// Safe even if either user or username is null
return user1 != null && user2 != null &&
Objects.equals(user1.getUsername(), user2.getUsername());
}
}
Why this works: Objects.equals(a, b) is a utility method that handles null values on both sides: it returns true if both are null or both are non-null and equal using .equals(). This eliminates the need for explicit null checks before comparison. The implementation is (a == b) || (a != null && a.equals(b)), providing optimal performance (fast reference check first) and null safety. This is the recommended approach when comparing two variables that might both be null.
Case-Insensitive Comparison
// SECURE - case-insensitive comparison
@GetMapping("/api")
public ResponseEntity<?> handleRequest(@RequestParam String method) {
// Use equalsIgnoreCase for case-insensitive comparison
if ("POST".equalsIgnoreCase(method)) { // Matches "post", "Post", "POST"
return handlePost();
}
if ("GET".equalsIgnoreCase(method)) {
return handleGet();
}
return ResponseEntity.badRequest().body("Unknown method");
}
// DON'T do this (less efficient, locale issues):
// if (method.toLowerCase().equals("post")) { } // Two operations
// if (method.toUpperCase().equals("POST")) { } // Two operations
// Use equalsIgnoreCase - single operation, no locale dependency
Why this works: equalsIgnoreCase() performs a case-insensitive comparison in a single operation, more efficient than converting to lowercase/uppercase and then comparing. It also avoids locale-specific issues where .toLowerCase() might behave differently in Turkish locale (where 'I'.toLowerCase() is 'ı' not 'i'). The constant-first pattern ("POST".equalsIgnoreCase(method)) provides null safety. This is ideal for comparing HTTP methods, file extensions, or other values where case shouldn't matter.
Constant-Time Comparison for Secrets
// SECURE - constant-time comparison for passwords/tokens
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
@Service
public class AuthService {
public boolean validatePassword(String providedPassword, byte[] storedHash) {
byte[] providedHash = hashPassword(providedPassword);
// SECURE - constant-time comparison (prevents timing attacks)
return MessageDigest.isEqual(providedHash, storedHash);
}
public boolean validateCSRFToken(String providedToken, String sessionToken) {
if (providedToken == null || sessionToken == null) {
return false;
}
// Convert to bytes for constant-time comparison
byte[] provided = providedToken.getBytes(StandardCharsets.UTF_8);
byte[] session = sessionToken.getBytes(StandardCharsets.UTF_8);
return MessageDigest.isEqual(provided, session);
}
private byte[] hashPassword(String password) {
// Use proper password hashing (bcrypt, Argon2, etc.)
// This is just an example
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(password.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
throw new RuntimeException("Hash failed", e);
}
}
}
Why this works: MessageDigest.isEqual() performs constant-time byte array comparison, taking the same amount of time whether values match or differ. This prevents timing attacks where attackers measure response times to guess passwords or tokens character-by-character. Regular .equals() can short-circuit (return early on first mismatch), leaking information about where values differ. For security-sensitive comparisons (passwords, CSRF tokens, API keys, HMAC signatures), constant-time comparison is essential. This method compares every byte regardless of mismatches, providing no timing information to attackers.
Testing and Verification
Verify String Comparison is Correct
Manual verification steps:
Search for == usage with Strings
Find potential bugs
# Find == comparisons (may include false positives)
grep -rn " == \"\| == '" src/ --include="*.java"
# More targeted search for String comparisons
grep -rn "String.*==\|==.*String" src/ --include="*.java"
# Review each instance - should use .equals() instead
Verify security-critical comparisons use .equals()
# Check authentication/authorization code
grep -A 5 -B 5 "role.*==\|permission.*==\|password.*==" src/
# Should use .equals(), not ==
Test with user input
Verify behavior with non-literal strings
// Manual test in development:
String userRole = request.getParameter("role"); // Returns new String
String expectedRole = "ADMIN";
// TEST 1: Using == (WRONG)
if (userRole == expectedRole) {
System.out.println("Granted");
} else {
System.out.println("Denied - == failed even though value is 'ADMIN'");
}
// TEST 2: Using .equals() (CORRECT)
if (expectedRole.equals(userRole)) {
System.out.println("Granted - .equals() works correctly");
}
Review security-sensitive code paths
# Find authentication/authorization logic
grep -r "authenticate\|authorize\|checkPermission\|hasRole" src/
# Review for == usage in those files
# Should use .equals() or .equalsIgnoreCase()
Static analysis
Use tools to detect == with Strings
# PMD rule: CompareObjectsWithEquals
# SpotBugs: ES_COMPARING_STRINGS_WITH_EQ
# SonarQube: S4973 (Strings should be compared with equals)
mvn pmd:check
mvn spotbugs:check
mvn sonar:sonar
Code review checklist
- No
==or!=used for String comparison -
.equals()used for exact match -
.equalsIgnoreCase()used for case-insensitive comparison - Constant-first pattern (
"ADMIN".equals(userRole)) for null safety -
Objects.equals()used when both values might be null - Enums used instead of Strings for fixed sets (roles, permissions)
- Security-sensitive comparisons reviewed and tested
Security Checklist
- No
==or!=used for string comparison in security-critical code - All string comparisons use
.equals()orObjects.equals() - Case-insensitive comparisons use
.equalsIgnoreCase() - Constant-first pattern used for null safety (
"constant".equals(variable)) - Password/token comparisons use
MessageDigest.isEqual()for constant-time comparison - Security-critical values (roles, permissions) use enums instead of strings
- Unit tests verify string comparison works with dynamically created strings
- Code review confirms no reference equality in authentication/authorization
- SAST tools (SpotBugs, SonarQube) configured to detect
==in string comparisons - Integration tests verify authorization works with database-loaded roles