Analysis of the Unauthenticated Database Backup Vulnerability in Craft CMS

Analysis of the Unauthenticated Database Backup Vulnerability in Craft CMS

Alex Rollin
Alex Rollin
January 6, 2026
Last updated : February 15, 2026
January 6, 2026

Your Craft CMS database backup contains everything: user credentials, API keys, customer data, and your site's complete content history. If someone can download that backup without logging in, you've handed over the keys to your entire application. Here's the uncomfortable truth: there's no single CVE for "unauthenticated database backup download" in Craft CMS. The vulnerability isn't a bug in Craft's code—it's a deployment mistake that leaves your /storage/backups directory accessible to anyone with a web browser and basic URL knowledge. This article covers exactly what creates this exposure, how recent Craft CMS vulnerabilities make it worse, and the specific server configurations you need to lock down your backups. You'll get working Apache and Nginx configurations, a verification checklist, and a clear understanding of why this matters more than ever given the 2024-2025 wave of Craft CMS exploits.

Prerequisites

Before implementing these fixes, you'll need:

Server access requirements:

  • SSH access to your web server
  • Ability to modify Apache or Nginx configuration files
  • Root or sudo privileges for system-level changes

Knowledge requirements:

  • Basic familiarity with your web server (Apache or Nginx)
  • Understanding of file paths and directory structure
  • Ability to restart web server services

Craft CMS environment:

  • Craft CMS 3.x, 4.x, or 5.x installation
  • Access to your project's file structure
  • Knowledge of where your storage/ directory lives relative to your webroot

Tools you'll use:

  • Terminal/SSH client
  • Text editor for configuration files
  • cURL or a web browser for testing

Understanding the Problem

Craft CMS stores database backups, logs, and runtime data in the storage/ directory. By default, Craft's project structure keeps this folder outside the public webroot:

/var/www/project/
├── config/
├── storage/          ← Contains backups, logs, runtime data
│   └── backups/
├── templates/
├── vendor/
└── web/              ← Only this should be publicly accessible
    └── index.php

The problem happens when deployments put storage/ inside the webroot, or when servers lack rules blocking direct access. An attacker can simply request:

curl https://yoursite.com/storage/backups/craft_backup_2025-01-01-120000.sql

And receive your complete database dump.

This isn't theoretical. CVE-2025-54417 specifically requires access to /storage/backups as part of its attack chain. Attackers who can write files to your backup directory and know your security key can execute arbitrary code through Craft's /updater/restore-db endpoint.

Step-by-Step Implementation

Step 1: Identify Your Current Directory Structure

First, determine whether your storage/ directory sits inside or outside your webroot.

Connect to your server and find your Craft installation:

# Find your Craft installation
find /var/www -name "craft" -type f 2>/dev/null

Check the relationship between storage/ and your webroot:

# From your Craft project root
ls -la

Look for this structure (safe):

./config/
./storage/
./web/           ← Document root points here only

Or this structure (dangerous):

./web/
./web/config/
./web/storage/   ← Storage inside webroot

Step 2: Test for Current Exposure

Before making changes, confirm whether your backups are currently accessible.

Check if backups exist:

ls -la storage/backups/

Test direct HTTP access from outside your server:

# Replace with your actual domain
curl -I https://yoursite.com/storage/backups/

# If you know a backup filename
curl -I https://yoursite.com/storage/backups/craft_backup_2025-01-01-120000.sql

Response codes to watch for:

  • 200 OK — Your backups are exposed (critical)
  • 403 Forbidden — Access blocked (good)
  • 404 Not Found — Either blocked or path doesn't exist (verify which)

Step 3: Move Storage Outside Webroot (Preferred Method)

Our experience shows that moving storage/ entirely outside the webroot eliminates this class of vulnerability completely. No server rules to maintain, no configuration to get wrong.

If you can restructure your deployment:

Create the new directory structure:

# From your project root (above web/)
mkdir -p /var/www/project/storage

Move existing storage contents:

mv /var/www/project/web/storage/* /var/www/project/storage/
rmdir /var/www/project/web/storage

Update your .env file or config/general.php to point to the new location:

// config/general.php
return [
    '*' => [
        // Explicitly set storage path outside webroot
        'basePath' => dirname(__DIR__) . '/storage',
    ],
];

Set correct ownership:

chown -R www-data:www-data /var/www/project/storage
chmod -R 750 /var/www/project/storage

Step 4: Block Access at the Web Server Level (Alternative Method)

If you cannot move storage/ outside webroot (shared hosting, platform constraints), block access through server configuration.

For Apache:

Create or edit /var/www/project/web/storage/.htaccess:

# Deny all access to storage directory
Require all denied

Or add to your main Apache virtual host configuration:

<VirtualHost *:443>
    ServerName yoursite.com
    DocumentRoot /var/www/project/web
    
    # Block access to storage directory
    <Directory "/var/www/project/web/storage">
        Require all denied
    </Directory>
    
    # Additional protection: block backup file extensions anywhere
    <FilesMatch "\.(sql|sql\.gz|sql\.zip|bak)$">
        Require all denied
    </FilesMatch>
</VirtualHost>

Restart Apache:

sudo systemctl restart apache2

For Nginx:

Add to your server block configuration:

server {
    listen 443 ssl;
    server_name yoursite.com;
    root /var/www/project/web;
    
    # Block storage directory completely
    location ^~ /storage/ {
        deny all;
        return 404;
    }
    
    # Block backup file extensions anywhere
    location ~* \.(sql|bak)$ {
        deny all;
        return 404;
    }
    
    # Your existing Craft configuration
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
}

Test and reload Nginx:

sudo nginx -t
sudo systemctl reload nginx

Step 5: Set File Permissions

Regardless of which method you chose, tighten file permissions on the storage directory:

# Set directory permissions
find /var/www/project/storage -type d -exec chmod 750 {} \;

# Set file permissions
find /var/www/project/storage -type f -exec chmod 640 {} \;

# Ensure correct ownership
chown -R www-data:www-data /var/www/project/storage

These permissions ensure:

  • Owner (web server): read, write, execute on directories; read, write on files
  • Group: read, execute on directories; read on files
  • Others: no access

Step 6: Configure Backup Storage Location

For additional protection, configure Craft to write backups to a location completely outside your web project:

# Create dedicated backup directory
sudo mkdir -p /var/backups/craft
sudo chown www-data:www-data /var/backups/craft
sudo chmod 750 /var/backups/craft

Update your backup scripts or automation to use this path. If you're using the built-in Craft backup commands:

./craft db/backup /var/backups/craft/

Common Mistakes to Avoid

Mistake 1: Relying only on .htaccess without verifying it works

We've found that many hosting environments disable .htaccess processing for performance reasons. Always test that your rules actually block access rather than assuming they do.

Mistake 2: Blocking /storage/ but not /storage/backups/

Some configurations use location matching that doesn't apply to subdirectories. Test the specific /storage/backups/ path, not just the parent directory.

Mistake 3: Using predictable backup filenames with no access controls

Craft's default backup naming includes timestamps (craft_backup_2025-01-01-120000.sql). These are easy to guess or enumerate. Even with access controls, consider using randomized backup names or storing backups with timestamps only in the filename metadata.

Mistake 4: Forgetting about directory indexing

If Apache's Options Indexes is enabled and your block rule fails, attackers can browse /storage/backups/ and see a list of all your backup files. Disable directory indexing:

<Directory "/var/www/project/web/storage">
    Options -Indexes
    Require all denied
</Directory>

Mistake 5: Not patching related Craft vulnerabilities

Blocking backup access reduces risk, but recent Craft CVEs create additional attack paths. An unpatched CVE-2025-32432 gives attackers RCE, at which point they can access any file on the server regardless of web server rules.

Testing and Verification Steps

Test 1: Direct URL access

# Test storage root
curl -I https://yoursite.com/storage/

# Test backups directory
curl -I https://yoursite.com/storage/backups/

# Test specific backup file (use an actual filename if you know one)
curl -I https://yoursite.com/storage/backups/test.sql

Expected responses: 403 Forbidden or 404 Not Found

Test 2: Directory enumeration

# Check if directory listing is disabled
curl https://yoursite.com/storage/backups/

Should not return a list of files.

Test 3: File permission verification

# On the server, check permissions
ls -la /var/www/project/storage/
ls -la /var/www/project/storage/backups/

# Verify no world-readable permissions (no 'r' in the third group)
stat -c "%a %n" /var/www/project/storage/backups/*

Test 4: Craft functionality

After making changes, verify Craft still works correctly:

  • Log into the Craft admin panel
  • Navigate to Utilities → System Report
  • Check for any file permission warnings
  • Run a test backup and confirm it completes successfully
./craft db/backup

Test 5: External vulnerability scan

Use Craft's built-in server check or an external scanner to verify your configuration:

# Craft's system report includes security checks
./craft utils/system-report

Patching Related Vulnerabilities

Securing your backups is one layer of defense. You also need to patch the recent Craft CVEs that make exposed backups more dangerous.

Critical updates required:

  • CVE-2025-32432 (Critical 10.0) — Fixed In: 3.9.15, 4.14.15, 5.6.17 — Unauthenticated RCE via image transforms
  • CVE-2025-54417 (High) — Fixed In: 4.16.3, 5.8.4 — RCE via backup restore endpoint
  • CVE-2025-23209 (High 8.0) — Fixed In: 4.13.8, 5.5.8 — RCE with compromised security key
  • CVE-2024-56145 (Critical 9.3) — Fixed In: 3.9.14, 4.13.2, 5.5.2 — RCE via argc/argv handling

Update Craft CMS:

composer update craftcms/cms
./craft migrate/all
./craft project-config/apply

After updating, rotate your security key if you have any reason to believe it may have been exposed:

# Generate new security key
php -r "echo 'CRAFT_SECURITY_KEY=' . bin2hex(random_bytes(32)) . PHP_EOL;"

Update your .env file with the new key.

Conclusion

Protecting your Craft CMS database backups comes down to two principles: keep sensitive files outside the webroot, and block access to anything that can't be moved. The server configurations in this guide address the immediate exposure risk, but they work best alongside current Craft CMS patches.

Working with teams has taught us that backup security often gets overlooked because it's not part of the application—it's infrastructure. But as CVE-2025-54417 demonstrates, attackers treat /storage/backups as a direct path to code execution when combined with other vulnerabilities.

If you're running Craft CMS and haven't audited your storage directory exposure, start with the curl tests in Step 2. That five-minute check tells you immediately whether this article applies to you. For teams managing multiple Craft installations or looking to implement proper backup handling as part of a broader security review, we can help you build an approach that covers both immediate fixes and ongoing monitoring.

Share this article