Paths and Metadata

Paths, Permissions and File Metadata

basename(), dirname() and pathinfo() only split strings; realpath() resolves ., .. and symlinks, or returns false if the target is missing. filesize(), fileperms(), is_readable() and friends wrap stat() and answer for the user PHP runs as, www-data under Apache 129 (Web Server Permissions). realpath() also keeps a user-supplied name inside a directory:

Metadata, the stat cache, and refusing names that escape a directoryPHP
<?php
declare(strict_types=1);
function inside(string $base, string $name): ?string {         // null if $name escapes $base
  $root = realpath($base);
  $path = realpath("$root/$name");
  return $path !== false && str_starts_with($path, $root . DIRECTORY_SEPARATOR) ? $path : null;
}
mkdir($dir = __DIR__ . '/covers');
file_put_contents($f = "$dir/dune.jpg", str_repeat('x', 2048)); chmod($f, 0640);
symlink('/etc/passwd', "$dir/link.jpg");                          // a planted symlink
printf("%s %s %o %d bytes, owner %s\n", basename($f, '.jpg'), filetype($f), fileperms($f) & 0777,
  filesize($f), posix_getpwuid(fileowner($f))['name']);
exec("(sleep 1; printf more >> $f) >/dev/null 2>&1 &");         // another process appends
echo filesize($f), ' -> '; sleep(2); echo filesize($f), ' -> ';  // the second is cached
clearstatcache(); echo filesize($f), "\n";
foreach (['dune.jpg', '../../etc/passwd', 'link.jpg'] as $n) {
  echo $n, ' => ', inside($dir, $n) ?? 'REFUSED', "\n";
}
Output
dune file 640 2048 bytes, owner dev
2048 -> 2048 -> 2052
dune.jpg => /home/dev/shop/covers/dune.jpg
../../etc/passwd => REFUSED
link.jpg => REFUSED

The mode is the octal notation of Permission Bits. PHP caches the last stat(), so the second filesize() misses the other process's append until clearstatcache(). The trailing separator in inside() stops /srv/covers matching /srv/covers-old; for a file not yet created, check realpath(dirname(...)). Path Traversal treats traversal as an attack.