Secure Frontend Uploads & Private Storage

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 by www-data).
  • Access Control htaccess: Embed a protective .htaccess file inside the private folder containing Deny from all to 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 allUsers Storage 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:

  1. Obfuscate Filename: Rename the user-supplied filename to a cryptographically secure UUIDv7 string. This prevents attackers from guessing the file's location.
  2. Save to Private Storage: Move the temporary uploaded file to your private directory.
  3. Persist Unified Media Record: Create a new Media active record, setting the visibility parameter 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-8463500ac881

When 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');