Drupal Site Loads but Uploaded Files Return 403: Where Permissions and Storage URLs Differ

A 403 on uploaded Drupal files traces to either file-system permissions or a mismatch between the configured and actual storage URL — not the same fix.

Uploaded files return a 403 error when a Drupal site's web server can serve files from the public files directory, but the file system permissions or the configured storage URL block direct access to that specific file. Drupal, a content management system, separates the files table in its database (which records metadata) from the actual files on disk, so a mismatch between the two layers produces exactly this symptom: pages load normally while images, PDFs, or other uploads fail. The root causes split into two categories.

One is a permissions problem at the file system or web server level — the file exists but the server process cannot read it, or a `.htaccess` or Nginx rule blocks the path. The other is a configuration mismatch between where Drupal thinks files live and where they actually resolve, often introduced by a changed base URL, a private files setup, or a migration between hosts. Each has a different fix, and treating a permissions issue as a URL issue (or vice versa) wastes troubleshooting time.

Table of Contents

Why Drupal Separates Pages From Files in the First Place

Drupal's public files directory, by default `sites/default/files`, is where uploaded images, documents, and other media are stored on disk. The web server (Apache or Nginx) serves these files directly, without Drupal's PHP code running on every request — that's a deliberate performance optimization, since routing every image through the full Drupal bootstrap would be slow. This means the web server's own permission rules, rewrite rules, and file ownership settings govern whether a file is reachable, independent of whether Drupal's database record for that file is correct.

A page request and a file request take different paths through the server stack. The page goes through PHP-FPM and Drupal's routing system; the file request, in the public files case, often goes straight to the file system. That split is exactly why a site can render fine — menus, content, admin pages all working — while a linked image 403s. The two requests never touch the same code path, so a bug or misconfiguration in one doesn't necessarily affect the other.

Permissions — The Server-Level Cause

A 403 from the web server almost always means "I found this file, but I'm not allowed to read it" or "a rule explicitly blocks this." Common permission causes in a Drupal context include: When permissions are the cause, the fix is corrective rather than configurational: reset ownership and permissions to match what the hosting environment expects for the web server user, and check any `.htaccess` or Nginx location block for overly broad deny rules. A hosting provider's documentation or support channel is the authoritative source for the exact user and permission values on a given server, since these vary by host.

  • Files or directories owned by the wrong user, so the web server process (running as `www-data`, `apache`, or similar) lacks read access.
  • Directory permissions set too restrictively — Drupal's files directory generally needs to be readable and executable (traversable) by the web server, typically `755` for directories and `644` for files, though exact values depend on hosting setup.
  • A `.htaccess` file in `sites/default/files` (Apache) with rules that deny access to certain extensions or paths, sometimes left over from a security hardening step that was applied too broadly.
  • SELinux or similar mandatory access control contexts on the server blocking access even when standard Unix permissions look correct — this is specific to certain Linux distributions and hosting environments.
  • Files uploaded by one deployment process (a migration script, a cron job, a different user account) ending up with ownership that doesn't match what the web server expects.

Storage URLs — The Configuration-Level Cause

The second category is a mismatch between the URL Drupal generates for a file and the URL that actually resolves on the server. Drupal builds file URLs based on several settings: the base URL, the public files path, and — for private files — a dedicated download route that checks access permissions before serving the file through PHP. Common sources of this mismatch: Distinguishing this from a permissions problem is straightforward: if the file exists and is readable at the correct path but the URL being requested is wrong or points to a protected location, the fix is in configuration — correcting the base URL, the files path setting, or how private files are referenced — not in file system permissions.

  • A site moved between domains, or between HTTP and HTTPS, without the base URL or a reverse proxy/CDN configuration being updated to match, so generated file URLs point somewhere the current server setup doesn't serve.
  • Private files (`sites/default/files/private` or a path outside the web root) being linked as if they were public files. Private files are deliberately not reachable by direct URL — they're served through `/system/files/…`, which requires Drupal's bootstrap and an access check. A direct link to the underlying path will 403 because the web server is correctly refusing to serve a file it was told to protect.
  • A CDN, reverse proxy, or load balancer rewriting or caching URLs in a way that no longer matches the origin server's actual file layout.
  • A multi-site or multi-environment setup (staging vs. production) where the files directory path differs, but content still references the old path from a database copy or migration.

A Practical Way to Tell Which One You're Facing

Because the two causes look identical to a site visitor (a 403 and nothing else), the diagnostic step is to separate the "where" question from the "why" question. Running through these checks in order — path match, then permissions, then public/private status, then origin-versus-proxy — isolates which of the two categories is responsible before any configuration change is made, which avoids fixing the wrong layer and masking the actual cause.

  • Confirm the actual file path on disk matches the path in the URL that's 403ing. If they don't match, it's a storage URL issue — check base URL settings, CDN/proxy rewrite rules, and whether the file is private rather than public.
  • If the paths match, check file and directory permissions and ownership directly on the server, along with any `.htaccess` or server block rules for that directory.
  • Check whether the file is in the public or private files directory. Public files should be reachable by direct URL; private files should not be, and a 403 on a private file path may be expected behavior rather than a bug.
  • Test with a newly uploaded file. If a brand-new upload also 403s, the problem is systemic (permissions on the directory, or a server rule) rather than specific to one file's ownership or metadata.
  • If using a CDN or reverse proxy, test the origin server directly (bypassing the CDN) to see whether the 403 originates at the origin or is introduced by the intermediary layer.

You Might Also Like