realpath
Expands a path into a canonical absolute path, resolving symlinks, ., .. and duplicate slashes.
realpath(string $path): string|false- Added in PHP 4.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$path | string | The path to canonicalise. An empty string is treated as the current directory. |
Return value
string|falseThe canonicalised absolute path on success, or false if the path does not exist or is not accessible.
Errors
Does not throw. Returns false when the path is missing or a directory in the chain lacks execute permission; an open_basedir violation also emits an E_WARNING.
realpath() turns any path into the one canonical absolute form of that path: relative segments are resolved against the current working directory, every ., .. and repeated slash is collapsed, symbolic links are followed to their final target, and trailing separators are dropped. Crucially it touches the filesystem — if the path does not exist, or a directory along the way is not traversable, it returns false.
When to reach for it
The security use is the important one. Any time a path comes from user input, resolve it and then verify the result is still inside the directory you meant:
$base = realpath('/srv/uploads');
$req = realpath('/srv/uploads/' . $userInput);
if ($req === false || !str_starts_with($req, $base . DIRECTORY_SEPARATOR)) {
throw new RuntimeException('Path outside base');
}String comparison alone is not enough, because a/../../etc/passwd and a symlink both look innocent until they are resolved.
What trips people up
The false return for non-existent paths surprises people who want to normalise a path they are about to create; normalise dirname($path) instead and re-append the base name. Results are also cached in the realpath cache, so a path changed by another process may be stale until clearstatcache(true). And because relative input resolves against the current working directory — which differs between CLI, FPM and cron — pass absolute paths in anything scheduled.
Example
When to use it
- Confirm a user-supplied download path stays inside the intended storage directory before serving it.
- Normalise config-supplied paths so cron, CLI and FPM all resolve them the same way.
- Compare two paths for equality when one goes through a symlink and the other does not.
Frequently asked questions
Why does realpath return false?
Because the path does not exist, a directory in the chain lacks execute permission for the PHP user, or an open_basedir or Phar restriction blocks it. It never returns a path for something that is not actually on disk.
Is realpath enough to prevent directory traversal?
Only when you also compare the result against your base directory. Resolve both, then verify the resolved path starts with the base plus a separator; realpath() by itself just normalises, it does not restrict.
What is the difference between realpath and pathinfo?
pathinfo() is pure string parsing and never touches disk, while realpath() hits the filesystem to resolve symlinks and .. segments and fails if the target is missing.
More examples
Containment check against traversal
The first two resolve to the same file inside uploads/ and are allowed; the traversal attempt either resolves outside the base or fails outright and is blocked. Comparing the resolved prefix — with the separator appended — is what makes this reliable.
<?php
mkdir('uploads');
file_put_contents('uploads/a.txt', 'ok');
$base = realpath('uploads');
foreach (['a.txt', '../uploads/a.txt', '../../etc/passwd'] as $req) {
$p = realpath('uploads/' . $req);
$inside = $p !== false && str_starts_with($p, $base . DIRECTORY_SEPARATOR);
printf("%-22s %s\n", $req, $inside ? 'ALLOWED' : 'BLOCKED');
}
unlink('uploads/a.txt'); rmdir('uploads');Normalising a path that does not exist yet
realpath() fails on the unwritten file but succeeds on its parent directory, so resolving the parent and re-appending the base name gives you a usable absolute destination. The last case still returns false, correctly, because the parent does not exist either.
<?php
function resolveTarget(string $path): string|false {
$dir = realpath(dirname($path));
return $dir === false ? false : $dir . DIRECTORY_SEPARATOR . basename($path);
}
mkdir('out');
var_dump(realpath('out/new.txt')); // false - not created yet
echo resolveTarget('out/new.txt'), "\n"; // absolute, still works
var_dump(resolveTarget('nowhere/x.txt')); // false - parent missing
rmdir('out');Working directory matters
Both calls print the same absolute path even though the relative inputs differ, because realpath() always resolves against the current working directory. That directory is not the same under cron or php-fpm as it is in your shell, which is why scheduled scripts should pass absolute paths.
<?php
mkdir('work');
file_put_contents('work/f.txt', 'x');
echo "cwd: ", getcwd(), "\n";
echo realpath('work/f.txt'), "\n";
chdir('work');
echo "cwd: ", getcwd(), "\n";
echo realpath('f.txt'), "\n"; // same file, different relative input
chdir('..');
unlink('work/f.txt'); rmdir('work');
Discussion