When building invoicing systems, many developers make the mistake of using predictable file naming patterns or exposing direct download URLs. This creates several security vulnerabilities:
- Unauthorized access through URL guessing
- Lack of download tracking and analytics
- No access control or expiration mechanisms
- Potential for hotlinking and bandwidth theft
The secure approach involves creating a proxy script that handles file delivery while keeping the actual file path hidden. Here's the basic workflow:
- Store files outside web root (or protect with .htaccess)
- Generate unique tokens for each download request
- Validate permissions before serving files
- Force download headers rather than display in browser
Here's a complete PHP solution with proper security measures:
<?php
// download.php - Secure file delivery script
// Validate request and check permissions
function authorizeDownload($fileId) {
// Implement your authentication logic here
// Could check session, database permissions, etc.
return true; // Simplified for example
}
// Secure file delivery
function deliverFile($filePath) {
if (!file_exists($filePath)) {
http_response_code(404);
die('File not found');
}
// Get file info
$fileName = basename($filePath);
$fileSize = filesize($filePath);
$mimeType = mime_content_type($filePath);
// Set headers
header("Content-Type: $mimeType");
header("Content-Disposition: attachment; filename=\"$fileName\"");
header("Content-Length: $fileSize");
header("Expires: 0");
header("Cache-Control: must-revalidate");
header("Pragma: public");
// Clear output buffer
ob_clean();
flush();
// Deliver file
readfile($filePath);
exit;
}
// Main execution
if (isset($_GET['id']) && authorizeDownload($_GET['id'])) {
$fileId = $_GET['id'];
// In production, you would:
// 1. Look up the actual file path from database
// 2. Verify user has permission to access
// Example storage location outside web root:
$basePath = '/var/secure/invoices/';
$filePath = $basePath . 'invoice_' . $fileId . '.pdf';
deliverFile($filePath);
} else {
http_response_code(403);
die('Access denied');
}
?>
For production systems, consider these additional measures:
- Implement download tokens with expiration
- Add rate limiting to prevent brute force attacks
- Log all download attempts for auditing
- Use X-Sendfile for better performance with large files
- Consider adding watermarking for sensitive documents
Other methods to consider depending on your requirements:
// Using PHP's readfile() with output buffering
ob_start();
readfile($securePath);
$content = ob_get_clean();
// Or streaming chunks for large files
$chunkSize = 1024 * 1024;
$handle = fopen($filePath, 'rb');
while (!feof($handle)) {
echo fread($handle, $chunkSize);
ob_flush();
flush();
}
fclose($handle);
When serving sensitive documents like invoices through direct file links (e.g., invoice-123.pdf
), you expose several security vulnerabilities:
- Predictable URL patterns allow brute-force access attempts
- No access control once the URL is known
- No logging of download activities
- Potential hotlinking issues
Here's a complete implementation that serves files securely through PHP:
<?php
// auth_check.php - Validate user session first
session_start();
if (!isset($_SESSION['authenticated'])) {
header('HTTP/1.0 403 Forbidden');
exit;
}
// serve_file.php - Protected file delivery
if (isset($_GET['file_id'])) {
$file_id = (int)$_GET['file_id'];
$base_path = '/var/www/protected_invoices/';
// Map IDs to actual filenames (could be DB query)
$file_mapping = [
1 => 'client_contract_2023.pdf',
2 => 'invoice_001_apple_inc.pdf'
];
if (isset($file_mapping[$file_id])) {
$file_path = $base_path . $file_mapping[$file_id];
if (file_exists($file_path)) {
header('Content-Description: File Transfer');
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="'.basename($file_path).'"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file_path));
readfile($file_path);
exit;
}
}
header('HTTP/1.0 404 Not Found');
}
?>
For production environments, consider these additional protections:
// Add these headers before file delivery
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
// Implement temporary access tokens
$expected_token = hash_hmac('sha256', $file_id, 'your_secret_key');
if (!hash_equals($expected_token, $_GET['token'] ?? '')) {
die('Invalid access token');
}
For dynamic systems with many files, use a database solution:
<?php
$pdo = new PDO('mysql:host=localhost;dbname=invoices', 'user', 'password');
$stmt = $pdo->prepare("SELECT file_path, original_name FROM invoices WHERE id = ? AND user_id = ?");
$stmt->execute([$_GET['id'], $_SESSION['user_id']]);
if ($row = $stmt->fetch()) {
if (file_exists($row['file_path'])) {
// Add download record to database
$log = $pdo->prepare("INSERT INTO download_logs (...) VALUES (...)");
$log->execute([...]);
// Serve file as previously shown
}
}
?>
For large files or high-traffic systems:
- Use
X-Sendfile
(Apache) orX-Accel-Redirect
(Nginx) for better performance - Implement proper caching headers for public but secured files
- Consider CDN integration with token authentication