Aug 21, 2026·7 min read·7 visits
Wagtail CMS evaluates HTTP ETag headers before enforcing authentication and authorization checks on document serve views, leaking whether specific documents match guessed SHA-1 hashes.
An information disclosure vulnerability in the document serving subsystem of Wagtail CMS allows unauthorized users to verify if private documents match guessed SHA-1 hashes due to improper order of authentication checks.
Wagtail CMS contains a document management subsystem that allows administrators and content creators to upload, manage, and serve files securely. This subsystem controls access to documents using permissions, group access restrictions, and password protection schemes to prevent unauthorized viewing of sensitive files. The default document-serving implementation relies on a central view to parse incoming document requests and verify whether the requesting user possesses the requisite privileges to download the resource.
The vulnerability within this subsystem is classified as CWE-280: Improper Handling of Insufficient Permissions or Privileges. It emerges from the system utilizing standard HTTP ETag header validation before verifying whether the user is authorized to access the file. The vulnerability exposes an information disclosure vector, functioning as a binary verification oracle that confirms whether a specific document has a given SHA-1 hash.
Because the request processing pipeline evaluates the document's file hash early, it exposes structural information. This order-of-operations security flaw bypasses access control mechanisms entirely under specific conditions. Security researchers and operators must understand the execution flow to identify where the logic breaks down inside the application framework.
To optimize bandwidth usage and reduce server load, Wagtail utilizes HTTP ETags for conditional caching. In Django applications, this is typically handled by the @etag decorator, which intercepts incoming HTTP GET requests containing validation headers such as If-None-Match or If-Match. The decorator executes an auxiliary database query to determine the file hash of the requested resource before executing the primary view function.
If the client-provided header matches the database-retrieved SHA-1 hash, Django halts further processing and immediately returns an HTTP 304 Not Modified status code. If the hashes do not match, Django passes execution to the target view function, which then runs security checks. Since the security and authentication checks reside entirely inside the view function rather than a pre-execution middleware layer, the HTTP status code differs based on whether the guessed hash is correct.
An unauthorized user can exploit this logical sequencing flaw. By submitting a request with a guessed hash, the user receives an HTTP 304 if the guess is correct, and an HTTP 302 or 403 if the guess is incorrect. This differential response behavior allows an attacker to systematically verify the presence and content of files on the system without authorization.
In vulnerable versions of Wagtail, the serve view in wagtail/documents/views/serve.py is directly decorated with @etag(document_etag). The document_etag utility function executes a database query to retrieve the document hash using the document_id. This evaluation occurs before the permission checks are executed within the body of the serve function, leaving the validation step entirely bypassed in the matching branch.
# Vulnerable implementation
def document_etag(request, document_id, document_filename):
Document = get_document_model()
if hasattr(Document, "file_hash"):
return (
Document.objects.filter(id=document_id)
.values_list("file_hash", flat=True)
.first()
)
@etag(document_etag)
def serve(request, document_id, document_filename):
Document = get_document_model()
doc = get_object_or_404(Document, id=document_id)
# Authentication and permission checks occur below this pointThe security patch, applied in commit 4f57d589dd81dca4306b5d45a7dce34c61087966, resolves the vulnerability by restructuring how the @etag decorator is evaluated. The patch removes the global decorator from the serve view. Instead, it defines an inner function named serve_local which is wrapped in @etag(get_etag). This inner function is executed only after all authentication, group access, and password validation checks have succeeded.
# Patched implementation (simplified excerpt)
def serve(request, document_id, document_filename):
Document = get_document_model()
doc = get_object_or_404(Document, id=document_id)
# All authentication and permission checks are executed here first
def get_etag(request):
try:
return doc.file_hash
except AttributeError:
return None
@etag(get_etag)
def serve_local(request):
# File serving logic runs here
return response
return serve_local(request)This code restructuring ensures that unauthorized requests are terminated during the initial execution of serve(), returning a redirect or permission-denied response before the serve_local inner function is invoked. Consequently, the conditional ETag evaluation is never executed for unauthorized requests, resolving the information disclosure vector.
To exploit this vulnerability, an attacker must identify the URL path of the document serving endpoint on the target Wagtail site. The default path pattern follows the structure /documents/<document_id>/<filename>. Since document IDs are typically sequential integers, an attacker can iterate through IDs to discover document records.
The attacker must construct a list of target SHA-1 file hashes. This list can include hashes of standard corporate templates, leaked files, known public forms, or software release binaries. The attacker sends a crafted HTTP GET request containing the targeted SHA-1 hash within the If-None-Match header.
If the server returns an HTTP 304 Not Modified status code, the attacker successfully confirms that the document with the targeted ID matches the specified SHA-1 hash. If the server returns an HTTP 302 Found (redirect to login) or HTTP 403 Forbidden response, the attacker knows that the document does not match the specified hash. Repeating this process with multiple candidate hashes allows an attacker to identify the precise contents of private files.
The direct technical consequence of this vulnerability is information disclosure. While it does not allow an attacker to read arbitrary contents of private files directly, it serves as a validation mechanism to confirm the presence of known documents. This exposure is critical if attackers seek to verify whether proprietary designs, confidential contracts, or legal agreements have been stored within a private library.
The CVSS v3.1 score is evaluated at 3.7 (Low Severity) with the vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N. The attack complexity is categorized as high because the attacker must have a pre-existing list of candidate SHA-1 hashes to test against the endpoint. The confidentiality impact is rated as low because file contents are not directly extracted.
From a regulatory compliance perspective, this flaw can violate data privacy standards such as GDPR or HIPAA if private records can be identified. Confirming the presence of specific medical or financial records using known hashes constitutes a privacy breach. Therefore, security teams should treat this finding with appropriate urgency despite its low CVSS score.
The primary remediation for this vulnerability is upgrading the Wagtail installation to a patched version. Development teams should identify the current branch of Wagtail deployed in their environments and update to version 7.0.9, 7.3.4, 7.4.3, or 8.0rc2, depending on the release branch. Upgrading ensures that the secure deferred ETag evaluation mechanism is enforced globally.
For organizations unable to immediately deploy software updates, temporary mitigations can be implemented at the network edge. Reverse proxies, load balancers, or Web Application Firewalls (WAFs) can be configured to strip the If-Match and If-None-Match HTTP headers from all requests routed to the /documents/ URL pattern. This prevents the upstream Django application from processing conditional ETags, forcing the view to run the permission checks first and return standard redirects.
Additionally, administrators can implement custom middleware to sanitise incoming requests or monitor application logs for anomalous traffic. A sudden spike in HTTP requests directed at sequential document IDs containing diverse If-None-Match header values indicates brute-force scanning activity. This scanning behavior can be detected and blocked using automated rate-limiting solutions.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Wagtail Wagtail | < 7.0.9 | 7.0.9 |
Wagtail Wagtail | >= 7.1, < 7.3.4 | 7.3.4 |
Wagtail Wagtail | >= 7.4, < 7.4.3 | 7.4.3 |
Wagtail Wagtail | == 8.0rc1 | 8.0rc2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-280 |
| Attack Vector | Network |
| CVSS v3.1 | 3.7 (Low) |
| Vulnerability Type | Information Disclosure |
| Exploit Status | Proof of Concept |
| Component | wagtail/documents/views/serve.py |
The application does not properly handle cases where an actor has insufficient permissions, allowing execution of critical operations before security validation occurs.
CVE-2026-109618 (dubbed PHP2Shell) is a critical vulnerability in the PHP engine core resulting from improper out-of-memory exception handling via longjmp(). When request allocation fails mid-processing, non-local control flow bypasses cleanup procedures, leaving memory structures uninitialized or in a use-after-free state during request shutdown.
CVE-2026-109619 (PHP2Shell) is a critical Use-After-Free vulnerability in the core PHP engine caused by improper global state cleanup during out-of-memory exception handling across persistent worker requests.
In Python virtualenv prior to version 21.14.9, activation script generators and configuration writers use Path.exists() before writing files. Because Path.exists() resolves symbolic links, it returns False for dangling symlinks. Consequently, virtualenv skips unlinking pre-planted symbolic links or hard links, writing data directly to target locations outside the intended virtual environment directory.
A remote code execution vulnerability exists in Microsoft .NET due to untrusted data deserialization within the PresentationCore component. The flaw stems from implicit auto-conversion flags during clipboard data retrieval operations.
An out-of-bounds write vulnerability in Microsoft .NET Framework, .NET runtime, and Visual Studio allows unauthenticated remote attackers to execute arbitrary code via crafted pixel-shader bytecode or auto-converted clipboard payloads.
ASP.NET Core contains a thread deadlock vulnerability (CWE-833) that allows an unauthenticated remote attacker to cause a denial of service over the network by exhausting server thread pool resources.