flawopen.com/Path Traversal/PHP

Path Traversal in PHP

High Severity CWE-22
ELI5

PHP is told to load 'page.php'. An attacker passes '../../../../etc/passwd', and PHP obligingly dumps the system password file right onto the webpage.

Key terms on this page
realpath()
Expands all symbolic links and resolves references to /./, /../ and extra / characters in the path, returning the canonicalized absolute path.
DIRECTORY_SEPARATOR
Predefined PHP constant containing the system directory separator ('/' on Linux, '\' on Windows).

What's happening

In PHP, include($base . '/' . $_GET['page']) or readfile() allows attackers to pass ../ tokens or PHP stream wrappers (php://filter) to read arbitrary files.

Real-world impact

Path traversal and Local File Inclusion (LFI) in PHP have historically accounted for thousands of Remote Code Execution bugs, including landmark CMS vulnerabilities.

CISA Cybersecurity Advisory & MITRE CVE repository.

Vulnerable vs. fixed

VULNERABLE
<?php
// VULNERABLE: Direct concatenation in file read
$base_dir = '/var/www/uploads';
$file = $_GET['file'];

// Attacker input: ../../../../etc/passwd
$path = $base_dir . '/' . $file;

// Dumps arbitrary server file to user
readfile($path);
?>
FIXED
<?php
// HARDENED: Canonicalize with realpath and enforce directory prefix check
$base_dir = realpath('/var/www/uploads');
$file = $_GET['file'] ?? '';

if (!is_string($file) || empty($file)) {
    http_response_code(400);
    exit('Invalid file parameter');
}

// 1. Resolve canonical path
$real_target = realpath($base_dir . DIRECTORY_SEPARATOR . $file);

// 2. Strict boundary check (ensure prefix matches base_dir + separator)
$expected_prefix = $base_dir . DIRECTORY_SEPARATOR;
if ($real_target === false || !str_starts_with($real_target, $expected_prefix)) {
    http_response_code(403);
    exit('Forbidden: Path Traversal detected');
}

if (!is_file($real_target)) {
    http_response_code(404);
    exit('File not found');
}

readfile($real_target);
?>

Why the fix works

realpath() resolves all traversal tokens. Checking str_starts_with($real_target, $base_dir . DIRECTORY_SEPARATOR) guarantees the resolved file sits strictly within the intended uploads directory.

Gotchas

realpath returns false for non-existent files

If realpath() returns false, the file does not exist or permissions prevent resolution. Always check for false before checking str_starts_with.

Common misconceptions

"basename() is all I need"

basename() strips directory paths, but also prevents users from accessing legitimate subfolders within an allowed directory tree.

How to check if you're affected

psalm --taint-analysis phpstan analyse

Prevention checklist

FAQ

Does PHP 8 still have null byte injection?

No. PHP 8.0+ has fully eliminated null byte (\0) truncation bugs in filesystem functions.

References