Skip to content

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