Files
wms-app/app/file.php
T
Thanakorn ae98dcdcdd Harden web root, secrets and realtime auth
- .htaccess: refuse .git, .env, docker, nodejs, sdlc*, scripts, archives and
  app internals; security headers, HSTS over TLS, optional HTTPS redirect
- uploads served through app/file.php to signed-in users only
- Apache/PHP hardening config for the container (ServerTokens, expose_php)
- least-privilege DB account and APP_SECRET_KEY via docker/php/provision.php;
  SMTP passwords re-encrypted with a random IV (secret_box.php)
- Socket.IO rooms from a PHP-signed token; /emit and cron refuse empty secrets
- escape notification text; CLI guards on build scripts; no fixed demo password
2026-09-24 14:53:40 +07:00

57 lines
1.8 KiB
PHP

<?php
/**
* file.php — serves files from app/uploads/ to signed-in users only.
*
* The root .htaccess rewrites every /app/uploads/<path> request here, so the
* existing <img src=".../uploads/profile/x.png"> URLs keep working but an
* anonymous visitor gets 401 instead of the file. Only the upload types that
* FileUploader accepts are served, and never anything that could execute.
*/
require_once __DIR__ . '/session.php';
if (empty($_SESSION['login_company_id'])) {
http_response_code(401);
exit;
}
// The session is only read from here on; release its lock so pages that load
// many images do not queue behind each other.
session_write_close();
$types = [
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
'pdf' => 'application/pdf',
];
$base = realpath(__DIR__ . '/uploads');
$rel = (string)($_GET['path'] ?? '');
$file = $base ? realpath($base . '/' . $rel) : false;
// realpath() resolves ../ and symlinks; anything outside uploads/ is refused.
if ($base === false || $file === false || !is_file($file) || strpos($file, $base . DIRECTORY_SEPARATOR) !== 0) {
http_response_code(404);
exit;
}
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
if (!isset($types[$ext])) {
http_response_code(404);
exit;
}
while (ob_get_level() > 0) {
ob_end_clean();
}
header('Content-Type: ' . $types[$ext]);
header('Content-Length: ' . filesize($file));
header('Content-Disposition: ' . ($ext === 'pdf' ? 'attachment' : 'inline') . '; filename="' . basename($file) . '"');
header('Cache-Control: private, max-age=3600');
header("Content-Security-Policy: default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; sandbox");
header('X-Content-Type-Options: nosniff');
readfile($file);