Moodle Private File Serving via pluginfile.php: Architecture Note
Moodle keeps user uploads outside the web root and serves them through pluginfile.php, which validates sessions, resolves context, checks capabilities, and streams files via the configured storage driver.
21 Sept 2026, 03:38 UTC

Requirements
Moodle must prevent direct URL access to user‑uploaded files while still allowing authorised users to download them. Access decisions are based on enrolment, role, and the specific context (course, module, user) of the file. Every download should be auditable, and the mechanism must work with whatever file storage driver Moodle is configured to use (local disk, NFS, S3, etc.).
Smallest Suitable Design
The core idea is to keep all user files outside the web‑accessible document root and serve them through a single, tightly‑controlled entry point: pluginfile.php. The flow is:
- Web server receives a request like
https://example.com/pluginfile.php/123/mod_forum/attachment/1/file.txt. pluginfile.phpvalidates the request:- Checks for a valid Moodle session or a signed service token.
- Parses the URL to extract the context id, component, filearea, itemid and filename.
- Loads the corresponding context and uses the File API to retrieve the file record.
- Evaluates the required capability (e.g.,
mod/forum:viewfor a forum attachment, orfile:accessallfilesfor administrators) against the user’s role assignments in that context. - If the check passes, streams the file using the configured storage driver; otherwise returns HTTP 403.
- Because the actual file bytes live under
$CFG->dataroot(the moodledata directory) which is outside the web root, there is no way to bypasspluginfile.phpby guessing a direct file URL.
Trust and Data Boundaries
Three logical boundaries define the trust model:
- Web server (untrusted perimeter) – receives raw HTTP requests; all URL parameters and headers are considered untrusted input.
- Moodle application (authorization boundary) – performs session/token validation, capability checks, and file‑record lookup.
- File storage driver (backend data boundary) – responsible for persisting and retrieving the actual bytes; it receives only an opaque file identifier from Moodle.
Public file areas (e.g., theme images) are stored in a separate storage area that bypasses capability checks; private user content must never be placed there.
Operational Checks
To verify that the design is working as intended, monitor the following:
- Response latency and error rate for
pluginfile.php(e.g., via web‑server logs or APM). Sudden spikes may indicate storage driver problems or expensive capability look‑ups. - File storage quota and orphaned files – run Moodle’s
admin/tool/filestorage/index.phpreport to detect files that exceed quota or are not referenced by any file record. - Capability assignments – periodically audit role overrides in contexts that host private files (e.g., forum modules) to ensure no unintended
file:accessallfilesgrants. - Access‑denied logging – Moodle writes an entry to
moodledata/log/wheneverpluginfile.phprejects a request; review these for patterns of misuse or mis‑configuration. - Token usage – if you enable temporary download URLs (e.g., for LTI tools), verify that signed tokens expire as expected and are not being replayed.
Failure Modes
- Storage driver outage – if the underlying disk, NFS mount, or object store becomes unavailable,
pluginfile.phpwill return 5xx errors, breaking file downloads. - Stale capability or session cache – Moodle caches role capabilities and session data; an outdated cache could grant or deny access incorrectly until the cache is refreshed.
- Misplaced files under webroot – accidental symlinks or manual uploads into the web‑accessible directory let users bypass
pluginfile.phpentirely. - Leaked signed or session tokens – a token that is intercepted can be used to download files until it expires; short lifetimes and HTTPS mitigate this risk.
Conditions That Would Change the Design
The minimal design remains appropriate as long as the core requirement is “serve private files through a single authorization gate”. The design would be revisited when:
- CDN offload is required** – you would need to generate time‑limited signed URLs for the CDN and store public assets in a CDN‑friendly bucket, while keeping private files behind Moodle’s gate.
- High concurrency or massive scale** – switching to an object storage driver that supports parallel reads (e.g., S3 with byte‑range requests) and possibly adding a reverse‑proxy cache for large files.
- Compliance mandates** – encryption at rest (via the storage driver or Moodle’s file encryption plugin) and immutable audit logs would affect the choice of driver and may introduce additional verification steps.
Practical Verification Steps
- Confirm that
$CFG->datarootpoints to a directory outside the web server’sDocumentRootand that the web server cannot list or serve files directly from it. - As an anonymous user, request a known private file URL (e.g., a forum attachment). Expect a 403 Forbidden response from
pluginfile.php. - Log in as a student enrolled in the relevant course and request the same URL; you should receive a 200 OK with the correct file contents.
- Review the file storage settings via
Site administration ► Server ► Filesto ensure the selected driver is reachable (test connection button). - Search Moodle logs for lines containing "pluginfile access denied" and verify that they correspond to expected failed attempts (e.g., from unknown users).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.