Filesystem Security

Yo Yogesh.M Updated 17 Oct 2026
6 min read ·Lesson 79 of 109
In this post

Why the Filesystem Needs Protection In simple terms: Your server's disk holds more than your web pages: configuration files with database passwords, private uploads, logs, and system files. Whenever P

Why the Filesystem Needs Protection

In simple terms: Your server's disk holds more than your web pages: configuration files with database passwords, private uploads, logs, and system files. Whenever PHP code reads, writes, includes, or deletes files based on user input, an attacker may try to reach files they should never see — for example, by requesting ../../config/.env. Filesystem security keeps file operations confined to the folders and files they are meant for.

Technically: The main risks are path traversal (using ../ or absolute paths to escape an intended directory), file inclusion (making include load attacker-chosen files), insecure permissions (files readable or writable by the wrong users or processes), race conditions between checking and using a file, and dangerous stream wrappers and functions. Defenses combine strict input validation, path resolution with realpath(), least-privilege permissions, and PHP configuration limits.


Path Traversal

The Vulnerability

// download.php?file=invoice-501.pdf — VULNERABLE
readfile('/var/www/shop/storage/invoices/' . $_GET['file']);

A request such as ?file=../../.env resolves to /var/www/shop/.env — the file with the database password. On Windows, backslashes and drive letters offer further variations.

The Defense: Validate the Name and Check the Resolved Path

<?php
function resolveInvoice(string $baseDir, string $requested): string
{
    if (!preg_match('/^invoice-\d{1,10}\.pdf$/', $requested)) {
        return 'rejected (invalid name)';
    }

    $base = realpath($baseDir);
    $path = realpath($base . DIRECTORY_SEPARATOR . $requested);

    if ($base === false || $path === false) {
        return 'not found';
    }

    if (!str_starts_with($path, $base . DIRECTORY_SEPARATOR)) {
        return 'rejected (outside the invoice folder)';
    }

    return 'allowed: ' . basename($path);
}

$baseDir = sys_get_temp_dir() . '/invoices-demo';
@mkdir($baseDir);
file_put_contents("$baseDir/invoice-501.pdf", '%PDF-demo');

$requests = [
    'valid name' => 'invoice-501.pdf',
    'traversal' => '../../etc/passwd',
    'missing file' => 'invoice-999.pdf',
    'NUL byte trick' => "invoice-501.pdf\0.txt",
];

foreach ($requests as $label => $requested) {
    echo str_pad($label, 15) . '→ ' . resolveInvoice($baseDir, $requested) . "\n";
}

Output

valid name     → allowed: invoice-501.pdf
traversal      → rejected (invalid name)
missing file   → not found
NUL byte trick → rejected (invalid name)

How It Works

  1. Strict validation first: the name must match exactly the expected pattern. ../, slashes, other extensions, and hidden characters never pass.
  2. realpath() resolves ., .., and symbolic links into the canonical absolute path, and returns false for files that do not exist.
  3. Containment check: the resolved path must begin with the base directory plus a separator (otherwise /invoices-demo-secret/... would pass a check for /invoices-demo).
  4. Old attacks using NUL bytes (file.pdf\0.php) to cut strings short in C functions no longer work in PHP 8, which rejects paths containing NUL bytes — but validating names strictly still stops them early.

Even better: identify files by database ID, check that the current user may access that record, and build the path only from values your application generated (see File Upload Security).


File Inclusion

Including files based on input is the most dangerous variant of path traversal, because the included file is executed:

// VULNERABLE
include __DIR__ . '/pages/' . $_GET['page'] . '.php';

Use an allow-list that maps input to fixed files (see PHP Include):

$pages = ['home' => 'home.php', 'about' => 'about.php'];
$page = $pages[$_GET['page'] ?? 'home'] ?? 'home.php';
include __DIR__ . '/pages/' . $page;

Keep allow_url_include = Off (the default), so include can never load code from remote URLs.


File Permissions and Ownership

The web server's PHP process (often www-data) should have only the access it needs:

PathTypical PermissionsReason
Application code (src/, public/, vendor/)Owned by a deploy user; 0644 files, 0755 directories; not writable by PHPAn attacker who gains code execution cannot modify the application
Configuration with secrets (.env)0640, readable by PHP's group only; outside the web rootNot downloadable, not readable by other users
Writable data (storage/, cache/, uploads)Writable by PHP, e.g., 0750 directories, 0640 filesRequired for operation; never executable
LogsWritable by PHP, readable by administratorsLogs often contain sensitive information

Practical Rules

  • Never use chmod 0777. It lets every user and process on the server read and modify the files.
  • Keep only public/ inside the web server's document root; configuration, storage, logs, and vendor/ stay outside.
  • Make sure dotfiles (.env, .git/) are never served — moving them out of the document root is the reliable solution.

Race Conditions: Check-Then-Use

Checking a file and then using it in a separate step leaves a gap in which another process (or attacker) can change it — a time-of-check to time-of-use (TOCTOU) problem:

// Race: the file might be created between the check and the write
if (!file_exists($path)) {
    file_put_contents($path, $data);
}

// Atomic: fopen() with 'x' fails if the file already exists
$handle = fopen($path, 'x');
if ($handle !== false) {
    fwrite($handle, $data);
    fclose($handle);
}

Use atomic operations where possible: fopen() mode x, flock(), writing to a temporary file and rename()-ing it (see PHP File Create/Write), and tempnam() / tmpfile() for temporary files with unpredictable names.


PHP Configuration Limits

SettingPurpose
open_basedir = /var/www/shop:/tmpRestricts all PHP file operations to listed directories — a useful extra boundary, especially on shared servers
allow_url_include = OffPrevents including remote code
allow_url_fopenAllows file_get_contents('https://...'); if enabled, never pass user-controlled URLs (SSRF risk)
disable_functions = exec,shell_exec,system,passthru,proc_open,popenRemoves shell access functions the application does not need

If the application must run external programs, pass every argument through escapeshellarg() — or, better, use proc_open() with an array of arguments (PHP 7.4+), which bypasses the shell entirely.


Stream Wrappers

PHP's file functions also accept stream wrappers such as php://, data://, phar://, http://, and ftp://. If a user-controlled string is passed to file_get_contents(), fopen(), include, or image functions, an attacker may use these wrappers to read local resources, make network requests, or (in older PHP versions) trigger unserialization through phar:// archives. PHP 8.0 no longer unserializes Phar metadata automatically, but the rule remains: never pass user input as a path or URL without strict validation.


Common Mistakes

Concatenating User Input Into Paths

Effect: Path traversal and file disclosure.

Fix: Validate names strictly, resolve with realpath(), and check containment — or use database IDs.

World-Writable Permissions

Effect: Any process can modify or plant files.

Fix: Grant minimal permissions; keep code read-only for PHP.

Secrets Inside the Web Root

Effect: .env, backups, or .git/ are downloadable.

Fix: Keep only public/ in the document root.

Check-Then-Use Without Atomic Operations

Effect: Race conditions and overwritten files.

Fix: Use exclusive modes, locks, and atomic renames.

Including Files From Input

Effect: Remote or local code execution.

Fix: Map input to an allow-list of files.


Best Practices

  • Never build filesystem paths directly from user input; validate, resolve, and confine them.
  • Keep the document root minimal and store secrets, storage, and logs outside it.
  • Apply least-privilege ownership and permissions; avoid 0777.
  • Use atomic file operations and secure temporary files.
  • Harden PHP with open_basedir, allow_url_include = Off, and disabled shell functions where possible.

Official reference: https://www.php.net/manual/en/security.filesystem.php

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Home PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else Slicing Strings User-defined Functions Slicing Strings PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators
Ask about this post
AI Ask about this post

Ask questions about Filesystem Security and get answers drawn from it.

Signed-in readers only.