GHSA-9WJX-4J4R-FF8W: Verified Reproduction
GHSA-9WJX-4J4R-FF8W: Reported Horilla protected media Referer authentication bypass
GHSA-9WJX-4J4R-FF8W is verified against horilla/horilla-hr · github. Vulnerability class: Auth Bypass. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00296.
What Is GHSA-9WJX-4J4R-FF8W?
GHSA-9WJX-4J4R-FF8W is a high-severity Auth Bypass vulnerability affecting horilla/horilla-hr. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00296).
GHSA-9WJX-4J4R-FF8W Severity
GHSA-9WJX-4J4R-FF8W is rated high severity.
High — serious impact or readily exploitable. Prioritize remediation.
How to Reproduce GHSA-9WJX-4J4R-FF8W
pruva-verify REPRO-2026-00296 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00296/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for GHSA-9WJX-4J4R-FF8W
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
Unauthenticated Referer header 'http://attacker.invalid/login' and private in-root /media/<path> URL
- /media/private-data/20260722T055646Z-13223/vuln-1-secret.txt
reproduction_steps.sh How the agent worked
Root Cause and Exploit Chain for GHSA-9WJX-4J4R-FF8W
- Affected version reproduced: tag
1.5.0, commit61bd5173220d19925ad8220db9152a75c881ea73. - Fixed version verified: tag
1.6.0, commitb3bd29d15819cbece45c58e6268ddd0614e387d6; source fix commitb6eaec1386d8b8741a42fe7c78f318f073375791is an ancestor of the fixed tag. - Risk level and consequences: high severity authorization bypass. An unauthenticated attacker who knows or can guess a private in-root media path can read that file without a cookie, session, bearer token, JWT, or any application credential. This run demonstrated unauthorized read of a randomly generated private canary under
MEDIA_ROOTand did not demonstrate traversal, account takeover, session forgery, or code execution.
Impact Parity
- Disclosed/claimed maximum impact:
authz_bypass/ unauthorized read of an exact nonpublic in-root media file via attacker-controlledRefereron the real Horilla/media/<path>route. - Reproduced impact from this run: full production-path
authz_bypass. Horilla 1.5.0 denied the private canary with noRefererbut returned HTTP 200 and the exact canary body when onlyReferer: http://attacker.invalid/loginwas added. Horilla 1.6.0 denied both requests while still serving a generated public liveness asset. - Parity:
full. - Not demonstrated: path traversal, broader arbitrary file read outside
MEDIA_ROOT, authenticated privilege changes, account takeover, session forgery, denial of service, or code execution.
Root Cause
In Horilla 1.5.0, protected_media(request, path) computes:
referer_path = urlparse(request.META.get("HTTP_REFERER", "")).path
and then skips the authentication/JWT check when referer_path is one of several public pages such as /login. Because Referer is fully controlled by the HTTP client and is not an authentication boundary, an unauthenticated attacker can set a cross-origin value whose path is /login. The view then falls through to:
return FileResponse(open(media_path, "rb"))
for any existing media path not under the small exempted folder list. The source evidence in bundle/artifacts/horilla_referer_bypass/20260722T055646Z-13223/source/protected_media_hunks.log shows this vulnerable logic in tag 1.5.0 and the fixed 1.6.0 logic. The fixed version removes public-page Referer authorization and instead permits only explicit public media path prefixes, uses safe_join(settings.MEDIA_ROOT, path), and requires an authenticated user or valid JWT for all nonpublic media paths. The known fix commit is b6eaec1386d8b8741a42fe7c78f318f073375791.
Reproduction Steps
- Run
bundle/repro/reproduction_steps.shfrom any directory, optionally withPRUVA_ROOT=/path/to/bundle. - The script reads
bundle/project_cache_context.json, reuses or creates/pruva/project-cache/repo, verifies the vulnerable and fixed tag commits, installs each tag's ownrequirements.txtinto isolated dependency directories, prepares migrated SQLite runtime templates, starts real Horilla Djangomanage.py runservercontainers, and sends attacker-processcurl --path-as-isrequests to the real/media/<path>route. - For each of two clean attempts per role, the script creates a new private canary under a nonpublic
MEDIA_ROOT/private-data/<run>/...path and a public liveness canary underMEDIA_ROOT/base/icon/.... It records requests, responses, curl traces, container logs, source identities, and modified-file inventories. - Expected evidence: vulnerable attempts show public liveness
200, baseline private request with noReferer302denied with no canary in the body, and spoofedReferer: http://attacker.invalid/login200with response body equal to the private canary. Fixed attempts show public liveness200while both baseline and spoofed private requests return302and do not contain the canary.
Evidence
- Main reproduction script:
bundle/repro/reproduction_steps.sh. - Latest successful runtime manifest:
bundle/repro/runtime_manifest.json. - Latest structured verdict:
bundle/repro/validation_verdict.json. - Latest complete successful proof directory:
bundle/artifacts/horilla_referer_bypass/20260722T055646Z-13223/. - Matrix summary:
bundle/artifacts/horilla_referer_bypass/20260722T055646Z-13223/matrix_summary.jsonwith"all_oracles_passed": true. - Source comparison:
bundle/artifacts/horilla_referer_bypass/20260722T055646Z-13223/source/protected_media_hunks.log. - Per-attempt request/response artifacts include:
- Vulnerable attempt 1 request pair:
attempts/vuln_1/baseline_no_referer_request.txtandattempts/vuln_1/trigger_spoofed_referer_request.txt. - Vulnerable attempt 1 body evidence:
attempts/vuln_1/private_canary.txtandattempts/vuln_1/trigger_spoofed_referer_response_body.binmatch exactly. - Fixed attempt controls:
attempts/fixed_1/attempt_result.jsonandattempts/fixed_2/attempt_result.jsonshow both private requests denied and the public liveness route working.
- Vulnerable attempt 1 request pair:
- Key latest matrix results:
{
"all_oracles_passed": true,
"attempt_results": [
{"role": "fixed", "attempt": 1, "public_status": "200", "baseline_status": "302", "trigger_status": "302", "oracle_passed": true},
{"role": "fixed", "attempt": 2, "public_status": "200", "baseline_status": "302", "trigger_status": "302", "oracle_passed": true},
{"role": "vuln", "attempt": 1, "public_status": "200", "baseline_status": "302", "trigger_status": "200", "oracle_passed": true},
{"role": "vuln", "attempt": 2, "public_status": "200", "baseline_status": "302", "trigger_status": "200", "oracle_passed": true}
]
}
- The final script revision was executed successfully twice consecutively. The latest run completed at
2026-07-22T06:00:10Zand the prior successful run completed at2026-07-22T05:56:08Z.
Recommendations / Next Steps
- Upgrade Horilla deployments to version 1.6.0 or later, or apply fix commit
b6eaec1386d8b8741a42fe7c78f318f073375791if maintaining a downstream branch. - Do not use
Referer,Origin, or other client-controlled metadata as an authorization decision for protected media. - Keep a strict allowlist of genuinely public media prefixes and require a real authenticated user/session or valid token for every other media path.
- Retain path containment checks such as Django
safe_join(settings.MEDIA_ROOT, path)and verify that only regular files underMEDIA_ROOTcan be opened. - Add regression tests at the real HTTP route level: absent
Referer, cross-origin/loginReferer, same-origin public pages, authenticated access, JWT access, and public-prefix liveness should all be covered.
Additional Notes
- Idempotency confirmation:
bundle/repro/reproduction_steps.shpassed two consecutive clean runs. Each run creates fresh random private and public canaries and clean per-attempt SQLite databases. - Runtime accommodations: to start the tagged application in this clean environment, the script installs each tag's dependencies and prepares SQLite migrations using
manage.py makemigrationsfor the Horilla project apps beforemigrate --run-syncdb. It records modified-file inventories. It does not modifybase/views.py,base/urls.py, routing, middleware, authentication logic, or file-open behavior. - The attacker requests are made by
curlfrom outside the Horilla Django process, do not follow redirects, and explicitly record that no cookie or authorization header is sent.
GHSA-9WJX-4J4R-FF8W Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for GHSA-9WJX-4J4R-FF8W
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix GHSA-9WJX-4J4R-FF8W
FAQ: GHSA-9WJX-4J4R-FF8W
Is GHSA-9WJX-4J4R-FF8W exploitable?
How severe is GHSA-9WJX-4J4R-FF8W?
How can I reproduce GHSA-9WJX-4J4R-FF8W?
Is the GHSA-9WJX-4J4R-FF8W reproduction verified?
References for GHSA-9WJX-4J4R-FF8W
Authoritative sources for GHSA-9WJX-4J4R-FF8W — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.