Weakness reference
CWE-41

Improper Resolution of Path Equivalence

Path equivalence weaknesses occur when software fails to recognize that different representations of the same file path should be treated identically. An…

01Summary

Path equivalence weaknesses occur when software fails to recognize that different representations of the same file path should be treated identically. An attacker can exploit this by using alternative path formats—such as adding trailing slashes, changing case, or using symbolic links—to bypass access controls that rely on exact string matching. This is particularly dangerous in security-sensitive contexts like file upload validation, access control lists, and URL routing.

02How It Happens

Most operating systems and filesystems treat certain path variations as equivalent. For example, /admin/config and /admin/config/ may point to the same resource, and on case-insensitive filesystems, /Admin/Config and /admin/config are identical. When developers implement access controls by comparing path strings directly without normalizing them first, they create gaps that attackers can exploit. The software may block access to /admin/ but allow /admin or /Admin, or it may reject a file upload with extension .php but accept .PHP. These inconsistencies arise because the validation logic and the underlying filesystem or web server have different rules for what constitutes an "equivalent" path.

03Real-World Impact

Successful exploitation can lead to unauthorized access to restricted files or directories, bypass of file upload restrictions, or circumvention of URL-based access controls. An attacker might upload a malicious script by changing its extension case, access sensitive configuration files by appending a trailing slash, or reach admin panels by using alternative path capitalizations. The severity depends on what resources are protected by the flawed path comparison—in high-value targets, this can result in full application compromise or data exposure.

04Vulnerable & Fixed Patterns

Vulnerable pattern
import os

BLOCKED_PATHS = ['/admin', '/config', '/private']

def is_path_allowed(user_path):
    for blocked in BLOCKED_PATHS:
        if user_path == blocked:
            return False
    return True

def serve_file(user_input):
    if is_path_allowed(user_input):
        with open(user_input, 'r') as f:
            return f.read()
    else:
        return "Access denied"

Why it's vulnerable:
The comparison uses exact string matching without normalizing the path. An attacker can bypass the check by using /admin/, /Admin, or other equivalent representations that the filesystem recognizes but the string comparison does not.

Fixed pattern
import os

BLOCKED_PATHS = ['/admin', '/config', '/private']

def is_path_allowed(user_path):
    # Normalize: resolve symlinks, remove redundant separators, convert to absolute
    normalized = os.path.normpath(os.path.abspath(user_path))
    normalized_lower = normalized.lower()
    
    for blocked in BLOCKED_PATHS:
        blocked_normalized = os.path.normpath(os.path.abspath(blocked)).lower()
        if normalized_lower == blocked_normalized or normalized_lower.startswith(blocked_normalized + os.sep):
            return False
    return True

def serve_file(user_input):
    if is_path_allowed(user_input):
        with open(user_input, 'r') as f:
            return f.read()
    else:
        return "Access denied"
Vulnerable pattern
<?php
$blocked_paths = array('/admin', '/config', '/private');

function is_path_allowed($user_path) {
    global $blocked_paths;
    foreach ($blocked_paths as $blocked) {
        if ($user_path === $blocked) {
            return false;
        }
    }
    return true;
}

if (is_path_allowed($_GET['file'])) {
    echo file_get_contents($_GET['file']);
} else {
    echo "Access denied";
}
?>

Why it's vulnerable:
Direct string comparison ignores path normalization. Requests like ?file=/admin/ or ?file=/Admin will bypass the check because they don't match the exact strings in the blocked list.

Fixed pattern
<?php
$blocked_paths = array('/admin', '/config', '/private');

function is_path_allowed($user_path) {
    global $blocked_paths;
    // Normalize: resolve to real path, handle case sensitivity
    $real_path = realpath($user_path);
    if ($real_path === false) {
        return false; // Path does not exist or is inaccessible
    }
    
    foreach ($blocked_paths as $blocked) {
        $blocked_real = realpath($blocked);
        if ($blocked_real && (strcasecmp($real_path, $blocked_real) === 0 || 
            strpos($real_path, $blocked_real . DIRECTORY_SEPARATOR) === 0)) {
            return false;
        }
    }
    return true;
}

if (is_path_allowed($_GET['file'])) {
    echo file_get_contents($_GET['file']);
} else {
    echo "Access denied";
}
?>

05Prevention Checklist

Normalize all paths before comparison:
Use realpath() (PHP), os.path.abspath() and os.path.normpath() (Python), or equivalent functions to resolve symlinks, remove redundant separators, and standardize case.
Use allowlists, not blocklists:
Define which paths *are* permitted rather than which are forbidden; this is more robust against equivalence tricks.
Validate against the canonical form:
Always compare the normalized, absolute path of user input against normalized, absolute allowed paths.
Test with path variants:
In your test suite, explicitly check that /path, /path/, /Path, and symlink variations are all handled consistently.
Avoid string-based path matching:
Do not rely on substring or regex matching of path strings; use filesystem APIs that understand path semantics.
Document path handling assumptions:
Clarify whether your application is case-sensitive, whether trailing slashes matter, and how symlinks are resolved.

06Signs You May Already Be Affected

Review your access control logs for requests using unusual path formats (e.g., repeated slashes, mixed case, or trailing slashes on normally blocked paths). Check whether your file upload validation rejects files based on extension alone without normalizing the filename first. If you have file-based access controls, test whether appending a trailing slash or changing case allows access to resources that should be blocked.

07Related Recent Vulnerabilities