Secure File Downloads
debt(d5/e5/b3/t7)
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.
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.
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.
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.
Also Known As
TL;DR
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
Why It Matters
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
// Path traversal + no auth check:
$file = $_GET['file']; // e.g. '../../config/database.php'
readfile('/var/www/uploads/' . $file); // Serves config file to attacker!
// 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'",
]);
}