CWE-35: Path Equivalence - PHP
Overview
Path equivalence vulnerabilities in PHP applications occur when user-supplied paths are validated using string functions like strpos() or str_contains() without canonicalization, allowing attackers to bypass restrictions through variations like ....//, ./, double slashes, or symbolic links. String concatenation for path construction doesn't prevent directory traversal or resolve path equivalents.
Primary Defence: Use realpath() to canonicalize all user-supplied paths (resolving ., .., symlinks, and normalizing separators), validate filenames don't contain path separators (/ or \\), verify the canonical path starts with the canonicalized allowed directory using strpos() === 0 or str_starts_with(), check that realpath() didn't return false (file must exist), and confirm it's a regular file with is_file() before access.
Common Vulnerable Patterns
String Contains Check Without Normalization
<?php
$base_dir = '/var/www/uploads/';
$file = $_GET['file'];
$full_path = $base_dir . $file;
// Weak validation - doesn't handle various equivalents
if (strpos($full_path, '..') === false) {
// Attack: file=....//....//etc/passwd (bypasses check)
// Attack: file=uploads/../../../etc/passwd
// Attack: file=./uploads/../../etc/passwd
readfile($full_path);
}
Why this is vulnerable:
strpos()check for..doesn't catch variations like....//or encoded sequences- String concatenation for path construction doesn't prevent directory traversal
- No path canonicalization means symbolic links and
.components aren't resolved - Path equivalence via double slashes (
//) or trailing slashes can bypass validation
Secure Patterns
RealPath with Prefix Validation
<?php
$base_dir = realpath('/var/www/uploads');
$file = $_GET['file'] ?? '';
if ($base_dir === false) {
http_response_code(500);
die('Upload directory unavailable');
}
if (empty($file)) {
http_response_code(400);
die('Filename required');
}
// Prevent path separators, traversal, or null bytes in filename (filename only, no dirs)
if (strpos($file, '/') !== false || strpos($file, '\\') !== false ||
strpos($file, "\0") !== false || basename($file) !== $file) {
http_response_code(400);
die('Invalid filename');
}
// Construct full path
$full_path = $base_dir . DIRECTORY_SEPARATOR . $file;
// Canonicalize path (resolves .., ., symlinks)
$real_path = realpath($full_path);
// Check if realpath succeeded (file exists)
if ($real_path === false) {
http_response_code(404);
die('File not found');
}
// Verify canonical path is within allowed directory
// Note: both paths are now canonical, safe to compare
if (strpos($real_path, $base_dir . DIRECTORY_SEPARATOR) !== 0) {
http_response_code(403);
die('Access denied');
}
// Verify it's a file
if (!is_file($real_path)) {
http_response_code(400);
die('Not a file');
}
readfile($real_path);
Why this works:
realpath()canonicalizes both base and requested paths, resolving symlinks and path components- Prefix check uses canonical paths on both sides, preventing equivalence bypasses
- Rejects filenames with path separators, traversal, or null bytes to prevent directory traversal
- Validates file exists (realpath returns false if not) and is a regular file
- All path operations use canonicalized forms, eliminating path equivalence attacks
Additional Resources
- CWE-35: Path Equivalence
- PHP realpath() Documentation - Canonicalize and resolve symlinks
- PHP Filesystem Functions - is_file(), readfile(), file_exists()
- OWASP Path Traversal