One Hat Cyber Team
Your IP :
104.23.243.58
Server IP :
172.67.218.182
Server :
Linux 128-201-239-36.cprapid.com 3.10.0-1160.41.1.el7.x86_64 #1 SMP Tue Aug 31 14:52:47 UTC 2021 x86_64
Server Software :
Apache
PHP Version :
7.4.33
Buat File
|
Buat Folder
Eksekusi
Dir :
~
/
home
/
juscatamarca
/
public_html
/
campusjxj
/
public
/
View File Name :
download.php
<?php /** * public/download.php * * Secure file delivery endpoint for class_files assets. * * Access rules: * - admin : can download any file that exists in class_files. * - student: only active files whose class is published and whose * course is not archived. * * Receives: GET ?id=<int> (class_files.id, NOT stored_name) * * Path-traversal mitigation: * 1. storage_path from the DB is joined with ROOT_PATH, never with user input. * 2. realpath() resolves any ./ or ../ sequences. * 3. The resolved absolute path is validated to start with UPLOADS_PATH. * * @package CampusCatamarca */ declare(strict_types=1); require_once dirname(__DIR__) . '/config/app.php'; require_once ROOT_PATH . '/helpers/functions.php'; require_once ROOT_PATH . '/helpers/auth.php'; require_once ROOT_PATH . '/config/database.php'; // ----------------------------------------------------------------------- // 1. Authentication — must be logged in to receive any file // ----------------------------------------------------------------------- require_login(); $user = current_user(); $role = (string) ($user['role'] ?? ''); // ----------------------------------------------------------------------- // 2. Input validation — ?id= must be a positive integer // ----------------------------------------------------------------------- $fileId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT, [ 'options' => ['min_range' => 1], ]); if ($fileId === false || $fileId === null) { http_response_code(400); exit('Solicitud inválida: el parámetro id es obligatorio y debe ser un número positivo.'); } // ----------------------------------------------------------------------- // 3. DB lookup with access-control per role // ----------------------------------------------------------------------- $file = null; if ($role === 'admin') { /* * Admins can download any file regardless of class/course status. * This allows previewing files before publishing a class. */ $file = db_fetch_one( "SELECT cf.id, cf.original_name, cf.stored_name, cf.mime_type, cf.storage_path, cf.file_extension, cf.file_size_bytes, cf.status FROM class_files cf WHERE cf.id = :id LIMIT 1", [':id' => $fileId] ); } elseif ($role === 'student') { /* * Students access requires ALL of the following simultaneously: * - file status = 'active' * - class status = 'published' * - course status ≠ 'archived' */ $file = db_fetch_one( "SELECT cf.id, cf.original_name, cf.stored_name, cf.mime_type, cf.storage_path, cf.file_extension, cf.file_size_bytes, cf.status FROM class_files cf INNER JOIN classes cl ON cl.id = cf.class_id INNER JOIN courses co ON co.id = cl.course_id WHERE cf.id = :file_id AND cf.status = 'active' AND cl.status = 'published' AND co.status <> 'archived' LIMIT 1", [ ':file_id' => $fileId, ] ); } else { // Unknown or future roles are denied outright. http_response_code(403); exit('Acceso denegado: tu rol no tiene permitido descargar archivos.'); } // ----------------------------------------------------------------------- // 4. Record existence check // (covers: file not found, student not enrolled, class not published) // ----------------------------------------------------------------------- if ($file === null) { http_response_code(404); exit('Archivo no encontrado o no tenés permisos para acceder a él.'); } // ----------------------------------------------------------------------- // 5. Build absolute path and prevent path traversal // ----------------------------------------------------------------------- $storagePath = (string) ($file['storage_path'] ?? ''); if ($storagePath === '') { http_response_code(500); exit('El archivo no tiene una ruta de almacenamiento registrada.'); } /* * storage_path is stored as 'uploads/class_files/<stored_name>' (relative * to ROOT_PATH). Join with ROOT_PATH to obtain the absolute path, then * resolve symlinks / ./ / ../ with realpath(). */ $absolutePath = ROOT_PATH . DIRECTORY_SEPARATOR . ltrim( str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $storagePath), DIRECTORY_SEPARATOR ); $resolvedPath = realpath($absolutePath); $uploadsReal = realpath(UPLOADS_PATH); /* * Guard 1: realpath returns false when the path does not exist on disk. */ if ($resolvedPath === false) { http_response_code(404); exit('El archivo no existe en el servidor. Es posible que haya sido eliminado manualmente.'); } /* * Guard 2: the resolved absolute path MUST be inside UPLOADS_PATH. * This neutralises any path-traversal attempt that could have slipped * through earlier checks (e.g. a manually edited storage_path in the DB). */ if ($uploadsReal === false || !str_starts_with($resolvedPath, $uploadsReal . DIRECTORY_SEPARATOR)) { http_response_code(403); exit('Ruta de archivo no permitida.'); } /* * Guard 3: must be a regular, readable file — not a directory or device. */ if (!is_file($resolvedPath) || !is_readable($resolvedPath)) { http_response_code(403); exit('El archivo no está disponible para lectura.'); } // ----------------------------------------------------------------------- // 6. MIME type resolution // Trust the stored value; fall back to finfo if empty. // ----------------------------------------------------------------------- $mimeType = trim((string) ($file['mime_type'] ?? '')); if ($mimeType === '') { $finfo = finfo_open(FILEINFO_MIME_TYPE); $mimeType = ($finfo !== false) ? (string) finfo_file($finfo, $resolvedPath) : 'application/octet-stream'; if ($finfo !== false) { finfo_close($finfo); } } // Extra safety: if finfo returned something empty or false, hard-default. if ($mimeType === '' || $mimeType === false) { $mimeType = 'application/octet-stream'; } // ----------------------------------------------------------------------- // 7. Content-Disposition // // PDFs and common images are served 'inline' (browser renders them). // Everything else is forced to 'attachment' (triggers a save dialog). // This prevents browsers from rendering potentially dangerous content // (e.g., HTML files) in the page context. // ----------------------------------------------------------------------- $inlineAllowed = [ 'application/pdf', 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', ]; $dispositionType = in_array($mimeType, $inlineAllowed, true) ? 'inline' : 'attachment'; // ----------------------------------------------------------------------- // 8. Sanitize original_name for the Content-Disposition header. // // - Keep only the filename portion (basename) — no directory components. // - Strip ASCII control characters (U+0000–U+001F, U+007F). // - RFC 6266: use filename* (UTF-8 encoded) as primary, filename as fallback. // ----------------------------------------------------------------------- $originalName = (string) ($file['original_name'] ?? ''); $originalName = basename(preg_replace('/[\x00-\x1F\x7F]/', '', $originalName) ?? ''); if ($originalName === '') { // Last-resort filename: use stored_name or a generic name. $originalName = basename((string) ($file['stored_name'] ?? 'download')); } /* * RFC 5987 / RFC 6266 encoding for non-ASCII filenames. * Example: filename*=UTF-8''Mi%20Examen.pdf */ $encodedName = rawurlencode($originalName); $contentDisposition = $dispositionType . '; filename="' . addslashes($originalName) . '"' . "; filename*=UTF-8''" . $encodedName; // ----------------------------------------------------------------------- // 9. Resolve file size // Prefer the value stored in DB (avoids a stat() call); fall back to // filesize() in case the record has 0 (e.g., legacy rows). // ----------------------------------------------------------------------- $fileSize = (int) ($file['file_size_bytes'] ?? 0); if ($fileSize <= 0) { $fileSize = (int) filesize($resolvedPath); } // ----------------------------------------------------------------------- // 10. Stream the file // Clear any output buffer first so headers aren't contaminated. // ----------------------------------------------------------------------- if (ob_get_level() > 0) { ob_end_clean(); } header('Content-Type: ' . $mimeType); header('Content-Disposition: ' . $contentDisposition); header('Content-Length: ' . $fileSize); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: SAMEORIGIN'); header('Referrer-Policy: strict-origin-when-cross-origin'); readfile($resolvedPath); exit;