realpath()
debt(d7/e3/b3/t7)
Closest to 'only careful code review or runtime testing' (d7). Semgrep/Psalm rules can flag file operations without realpath() but the subtle bug (missing base-dir prefix check, unchecked false return) typically requires code review to catch reliably.
Closest to 'simple parameterised fix' (e3). Per quick_fix, the remediation is adding realpath() plus a str_starts_with() base-dir check at each file-access site — a pattern replacement, not a one-liner, since false-return handling must also be added.
Closest to 'localised tax' (b3). Applies to file-handling code paths only; doesn't shape the whole system but creates a recurring check pattern wherever user paths touch the filesystem.
Closest to 'serious trap' (t7). The misconception is exactly that realpath() alone sanitises paths — it doesn't; it also returns false for non-existent paths which silently breaks naive comparisons. Contradicts the intuitive 'canonical = safe' mental model.
Also Known As
TL;DR
Explanation
realpath($path) resolves all symbolic links, /../ and /./ sequences, and redundant separators to produce the canonical absolute path. It returns false if the file doesn't exist. The standard path traversal guard is: $real = realpath($base . '/' . $userInput); if ($real === false || strpos($real, $base) !== 0) { deny; }. This ensures the resolved path starts with the intended base directory, regardless of what the user submitted.
Common Misconception
Why It Matters
Common Mistakes
- Checking path prefix before calling realpath() — traversal sequences pass the check but resolve differently.
- Not handling realpath() returning false for non-existent paths — the comparison silently fails.
- Using realpath() without checking that the result starts with the intended base directory.
- Trusting that realpath() sanitises the path for security without the subsequent base-directory check.
Code Examples
// Check before realpath — bypassable:
$path = '/var/www/uploads/' . $_GET['file'];
if (str_starts_with($path, '/var/www/uploads/')) {
readfile($path); // ../../etc/passwd passes the check!
}
// Correct:
$real = realpath('/var/www/uploads/' . $_GET['file']);
if ($real === false || !str_starts_with($real, '/var/www/uploads/')) die('Denied');
readfile($real);
// realpath() resolves ../, ./, symlinks to canonical absolute path
// Returns false if path doesn't exist
$base = realpath('/var/www/uploads');
$path = realpath($base . '/' . $_GET['file']);
if ($path === false) abort(404); // file doesn't exist
if (!str_starts_with($path, $base . DIRECTORY_SEPARATOR)) abort(403); // path traversal
readfile($path);
// basename() strips directory components:
basename('../../../etc/passwd'); // 'passwd'
$safe = '/var/www/uploads/' . basename($_GET['file']); // still need realpath() too