← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

Secure File Downloads

security PHP 5.0+ Intermediate
debt(d5/e5/b3/t7)
d5 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'specialist tool catches it' (d5). The detection_hints list semgrep and psalm — specialist SAST tools — as the means of catching patterns like readfile($_GET['file']) without path validation. A default linter won't flag this; it requires a configured SAST rule to detect user-controlled paths flowing into file-streaming functions.

e5 Effort Remediation debt — work required to fix once spotted

Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix requires replacing a path-based approach with a database-mapped file ID approach, setting Content-Disposition headers, validating absolute paths with realpath(), and adding authorisation checks. This is more than a one-line swap — it touches the download endpoint logic, potentially a database schema for file IDs, and auth middleware — but is contained to the file-serving component rather than being fully cross-cutting.

b3 Burden Structural debt — long-term weight of choosing wrong

Closest to 'localised tax' (b3). The applies_to context is web only, and file download security is a concern scoped to download endpoints. It imposes a persistent design constraint on those endpoints (always use ID-based lookups, always validate paths), but the rest of the codebase is largely unaffected unless file serving is pervasive.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7). The misconception field states that hiding the real filename in the URL is considered sufficient security — developers familiar with URL obfuscation or signed URLs from other frameworks may believe obscurity is a valid control. This directly contradicts the correct mental model (backend must validate every request). Combined with common mistakes like passing user-controlled filenames directly to readfile() without realpath() validation, this is a serious trap that contradicts safer patterns developers may have internalized from other contexts.

About DEBT scoring →

Also Known As

file download secure download path traversal download

TL;DR

Preventing path traversal, unauthorised access, and content injection when serving file downloads — validating paths, checking authorisation, and setting correct headers.

Explanation

File download vulnerabilities: path traversal (user-supplied filename contains ../ to escape the intended directory), authorisation bypass (direct URL access to files without checking permissions), MIME sniffing (browser executes downloaded HTML as a script), and directory listing (web server exposes the file directory). Mitigations: resolve realpath() and verify it starts with the allowed base directory, check the user has permission to access the specific file, set Content-Disposition: attachment to force download, set Content-Type correctly with X-Content-Type-Options: nosniff, and never serve files from within the webroot.

Common Misconception

Hiding the real filename in the URL is sufficient security — obscurity is not a control; the backend must validate every download request against the authenticated user's permissions.

Why It Matters

A file download endpoint without path validation lets attackers download /etc/passwd, application config files with database credentials, and private keys — path traversal to sensitive files is trivial without realpath() validation.

Common Mistakes

  • User-controlled filename passed directly to readfile() — path traversal vulnerability.
  • No authorisation check — any authenticated user can download any file.
  • Serving files from within the webroot — direct URL access bypasses the download script.
  • Missing Content-Disposition: attachment — browser may render HTML or execute scripts.

Code Examples

✗ Vulnerable
// Path traversal + no auth check:
$file = $_GET['file']; // e.g. '../../config/database.php'
readfile('/var/www/uploads/' . $file); // Serves config file to attacker!
✓ Fixed
// Secure download:
public function download(int $fileId): Response {
    $file = $this->files->findOrFail($fileId);

    // Authorisation: does this user own this file?
    if ($file->user_id !== auth()->id()) abort(403);

    // Path validation: realpath prevents traversal:
    $base    = realpath(storage_path('uploads'));
    $full    = realpath($base . '/' . $file->stored_name);
    if (!$full || !str_starts_with($full, $base . DIRECTORY_SEPARATOR)) {
        abort(400, 'Invalid file path');
    }

    return response()->download($full, $file->original_name, [
        'Content-Type'              => $file->mime_type,
        'X-Content-Type-Options'    => 'nosniff',
        'Content-Security-Policy'   => "default-src 'none'",
    ]);
}

Added 16 Mar 2026
Edited 22 Mar 2026
Views 26
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings F 1 ping S 2 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 2 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 1 ping S 1 ping S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F
No pings yet today
No pings yesterday
Amazonbot 7 Perplexity 3 Google 2 Unknown AI 2 Ahrefs 2 SEMrush 1 Bing 1
crawler 17 crawler_json 1
DEV INTEL Tools & Severity
🟠 High ⚙ Fix effort: Medium
⚡ Quick Fix
Never stream files by path — map user requests to file IDs in a database, serve via readfile() with validated absolute paths, and set Content-Disposition: attachment to prevent browser execution
📦 Applies To
PHP 5.0+ web
🔗 Prerequisites
🔍 Detection Hints
readfile($_GET['file']); Content-Type not set explicitly; no path validation before streaming; user can download arbitrary server files
Auto-detectable: ✓ Yes semgrep psalm
⚠ Related Problems
🤖 AI Agent
Confidence: High False Positives: Medium ✗ Manual fix Fix: Medium Context: Function Tests: Update
CWE-22 CWE-552

✓ schema.org compliant