security

Filesystem Security

Filesystem code must keep untrusted values away from arbitrary paths. Reads, writes, deletes, archive extraction, permissions, and public web serving all need deliberate boundaries.

Core Controls

  • Build paths beneath a controlled base directory.
  • Reject traversal segments and verify resolved paths stay under the base directory.
  • Use least-privilege filesystem permissions for the PHP process.
  • Keep secrets, logs, cache files, and private uploads outside the public web root.
  • Avoid broad recursive deletes unless the target path is tightly controlled.

Resolve Stored Keys, Not User Paths

A private download route should accept an application ID, load an authorised database record, and use its controlled storage key.

PHP example
<?php

declare(strict_types=1);

function safeStorageKey(string $key): bool
{
    return $key !== ''
        && !str_contains($key, '..')
        && !str_starts_with($key, '/')
        && !str_contains($key, "\0");
}

var_dump(safeStorageKey('invoices/2026/invoice-1001.pdf'));
var_dump(safeStorageKey('../../.env'));

// Prints:
// bool(true)
// bool(false)

This check is one layer. For existing local files, resolve the final path and verify it remains under the expected base directory before reading it.

ZIP entries can contain traversal paths. Symlinks can change where a seemingly safe path points. Inspect archive entries before extraction and avoid following links across a protected boundary.

Apply Least Privilege

The PHP process should write only the directories it needs, such as cache, logs, and uploads. Source code and secrets should not become writable merely to make a deployment error disappear.

In Application Work

Do not assume a cleaned filename alone is enough. Symlinks, archive entries, deployment permissions, and web-server configuration can change the effective boundary.

What To Check

Before moving on, make sure you can use database-owned storage keys, keep resolved paths beneath a base directory, review symlinks and archive entries, and apply least-privilege permissions.

Practice

Practice: Review A Private Download

Requirements

  • Use a database record to choose the stored key.
  • Authorise the current user before reading.
  • Resolve beneath a private storage directory.
  • Reject traversal and missing-file cases.
Show solution

Review Points

  • Build paths beneath a controlled base directory.
  • Reject traversal segments and verify resolved paths stay under the base directory.
  • Use least-privilege filesystem permissions for the PHP process.
  • Keep secrets, logs, cache files, and private uploads outside the public web root.
  • Avoid broad recursive deletes unless the target path is tightly controlled.