1. High-Level Architectural Summary
Zero CMS features a highly secure, unified public/private media storage architecture. This system enables developers to support secure file uploads from frontend forms (such as resumes, CVs, confidential agreements) that are public-uploadable but restricted strictly to logged-in back-office administrators. By combining secure metadata fields directly into the unified media table and isolating files inside a protective private storage directory, Zero CMS completely eliminates directory crawling, direct URL-guessing, and malicious script execution (backdoors) exploits.
2. Configuration and Storage Setup
To support secure private uploads locally, ensure your server has a protected private directory on disk that cannot be accessed directly via HTTP requests:
- Local Storage Directory: Create the directory
/data/misc/zero/storage/private/on your disk, applying defensive group permissions (chmod 0775, owned bywww-data). - Access Control htaccess: Embed a protective
.htaccessfile inside the private folder containingDeny from allto block Apache/Nginx from direct web-serves:# /data/misc/zero/storage/private/.htaccess Deny from all - Cloud Storage Buckets: If using GCS or S3, ensure that public access (e.g. the
allUsersStorage Object Viewer role) is strictly disabled on your private directories or private buckets.
3. Extension Developer Guide: Uploading Private Files
To implement private attachments inside your extension controllers or forms, execute the following three secure upload steps:
- Obfuscate Filename: Rename the user-supplied filename to a cryptographically secure UUIDv7 string. This prevents attackers from guessing the file's location.
- Save to Private Storage: Move the temporary uploaded file to your private directory.
- Persist Unified Media Record: Create a new
Mediaactive record, setting thevisibilityparameter to'private':
use Zero\Models\Media;
// 1. Move temporary file to secure private storage folder
$uuid = \Zero\Support\Security::uuidv7();
$diskName = "{$uuid}.bin";
$privatePath = "/storage/private/{$diskName}";
move_uploaded_file($_FILES['cv_file']['tmp_name'], APPLICATION_ROOT . $privatePath);
// 2. Persist metadata as private Media record
$media = new Media([
'filename' => $diskName,
'path' => $privatePath,
'mime' => $_FILES['cv_file']['type'],
'title' => 'Candidate CV - ' . $_POST['candidate_name'],
'visibility' => 'private', // Strict Gated Visibility!
'submission_id' => $submissionId, // Link to Form Submission
'original_name' => $_FILES['cv_file']['name'],
'file_size' => $_FILES['cv_file']['size']
]);
$media->save();4. Extension Developer Guide: Secure Retrievals & Gated Streams
Once saved as private, the Media model automatically handles secure gated routing. Calling the public helper $media->getUrl() will intercept and return the secure download route /admin/secure-download/{id} instead of the physical path:
// Unified Auto-Routing Helper
$url = $media->getUrl();
// Returns: /admin/secure-download/019efb8-2280-7904-9b2c-8463500ac881When an administrator clicks the link, the core SecureDownloadController intercepts the request, enforces administrative session and roles authentication, verifies multi-tenant boundaries (ensuring Site A editor cannot download Site B files), and streams the original file bytes directly to the browser with protective, non-cacheable headers:
# Secure Gatekeeper Stream Headers
header('Content-Type: ' . $file['mime_type']);
header('Content-Disposition: attachment; filename="' . basename($file['original_name']) . '"');
header('Cache-Control: private, must-revalidate');