Skip to content

GHSA-x52c-5hrq-76pq: Verified Reproduction

GHSA-x52c-5hrq-76pq: Horilla HRMS protected media path traversal enables outside-root file read

GHSA-x52c-5hrq-76pq is verified against horilla/horilla-hr · github. Affected versions: >=1.3.1, <1.6.0. Fixed in 1.6.0. Vulnerability class: Path Traversal. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00295.

REPRO-2026-00295 horilla/horilla-hr · github Path Traversal Jul 25, 2026 .txt
Severity
HIGH
CVSS
7.5
Confidence
HIGH
Reproduced in
87m 3s
Tool calls
743
Spend
$75.87
01 · Overview

What Is GHSA-x52c-5hrq-76pq?

GHSA-x52c-5hrq-76pq is a high-severity Path Traversal vulnerability affecting horilla/horilla-hr >=1.3.1, <1.6.0. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00295).

02 · Severity & CVSS

GHSA-x52c-5hrq-76pq Severity & CVSS Score

GHSA-x52c-5hrq-76pq is rated high severity, with a CVSS base score of 7.5 out of 10.

HIGH threat level
7.5 / 10 CVSS base
Weakness CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

High — serious impact or readily exploitable. Prioritize remediation.

03 · Affected Versions

Affected horilla/horilla-hr Versions

horilla/horilla-hr · github versions >=1.3.1, <1.6.0 are affected.

How to Reproduce GHSA-x52c-5hrq-76pq

$ pruva-verify REPRO-2026-00295
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00295/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for GHSA-x52c-5hrq-76pq

Information disclosure — reproduced
  • 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
Trigger

Raw /media/../../runtime/<fresh-canary> request target; Referer: http://attacker.invalid/login only for the unauthenticated composition

Attack chain
  1. Separate attacker container
  2. TCP HTTP
  3. real Horilla /media/<path> route
  4. base.views.protected_media
  5. outside-root canary FileResponse
How the agent worked 1,348 events · 743 tool calls · 1h 27m
1h 27mDuration
743Tool calls
105Reasoning steps
1,348Events
23Dead-ends
Agent activity over 1h 27m
Support
34
Repro
812
Judge
54
Variant
443
Verify
1
0:0087:03

Root Cause and Exploit Chain for GHSA-x52c-5hrq-76pq

Versions: component: Horilla HRMS base.views.protected_media, exposed by the real ^media/(?P<path>.*)$ route.Fixed: target validated: tag 1.6.0, commit b3bd29d15819cbece45c58e6268ddd0614e387d6.

Horilla HRMS 1.5.0 constructs the filesystem path for its real /media/<path> endpoint with os.path.join(settings.MEDIA_ROOT, path) and opens the result without verifying that canonical resolution remains beneath MEDIA_ROOT. A low-privilege user can therefore send .. segments in an HTTP request target and read a process-readable file outside the media directory. The same release also trusts the path portion of the attacker-controlled Referer header for media authorization; Referer: http://attacker.invalid/login is treated as a public-page request and composes with traversal into an unauthenticated outside-root read. Two consecutive executions of the final reproducer confirmed both paths against exact Horilla 1.5.0 source and confirmed fail-closed behavior in unchanged Horilla 1.6.0, using only fresh harmless canaries.

  • Affected component: Horilla HRMS base.views.protected_media, exposed by the real ^media/(?P<path>.*)$ route.
  • Affected target validated: tag 1.5.0, commit 61bd5173220d19925ad8220db9152a75c881ea73.
  • Fixed target validated: tag 1.6.0, commit b3bd29d15819cbece45c58e6268ddd0614e387d6.
  • Risk: High-severity information disclosure.
  • Consequence: A regular authenticated user can escape MEDIA_ROOT. Combined with the Referer authorization defect, an unauthenticated remote requester can read a file readable by the Horilla process. The proof is deliberately limited to fresh outside-root canaries; it does not read configuration, databases, source secrets, /proc, password files, or reusable credentials.

Impact Parity

  • Disclosed/claimed maximum impact: Remote information leak through the attacker-facing Horilla endpoint, including the unauthenticated traversal-plus-Referer composition.
  • Reproduced impact: In each of two consecutive complete script executions, two fresh Horilla 1.5.0 target instances returned HTTP 200 and the exact outside-root canary for (1) an authenticated low-privilege traversal and (2) a no-cookie/no-Authorization traversal carrying the crafted Referer. Two fresh Horilla 1.6.0 instances returned HTTP 404 without the outside canary, while authenticated in-root controls remained HTTP 200.
  • Parity: full for the submitted bounded info_leak claim.
  • Not demonstrated: Account takeover, session forgery, file modification, code execution, server compromise, or readability of every process-readable path through every proxy deployment.

Root Cause

Horilla 1.5.0 implements protected_media(request, path) as follows:

media_path = os.path.join(settings.MEDIA_ROOT, path)
if not os.path.exists(media_path):
    raise Http404("File not found")
...
return FileResponse(open(media_path, "rb"))

os.path.join() combines path strings but does not enforce containment. When path includes enough ../ components, filesystem resolution escapes MEDIA_ROOT. The route captures the remainder of /media/, and the reproducer uses HTTPConnection.putrequest() with a raw target so no client normalization removes those segments. Live Django access logs show the same traversal target observed after the TCP boundary.

The authorization logic independently derives referer_path from request.META["HTTP_REFERER"] and treats paths such as /login as public. Since only the path is compared, an attacker-controlled origin such as attacker.invalid is ignored. Supplying a Referer whose path is /login therefore skips authentication, and traversal plus this bypass yields an unauthenticated read in one request.

The containment fix is commit 67ac2056813ee95d4c4a0bfe7c0124a361cb6c48. It replaces string joining with Django's safe_join(settings.MEDIA_ROOT, path), maps rejected traversal to HTTP 404, and requires a regular file. Horilla 1.6.0 additionally removes Referer-based authorization in favor of explicit public-media prefixes.

Reproduction Steps

  1. Run PRUVA_ROOT=/path/to/bundle bundle/repro/reproduction_steps.sh from any working directory.
  2. The script reads bundle/project_cache_context.json and uses <project_cache_dir>/repo when prepared. It resolves the exact commits, proves the vulnerable commit lacks safe_join, proves the fixed release contains it, and creates clean git-archive source trees.
  3. It installs each tag's exact requirements.txt, generates Horilla's startup migrations (the releases ship migration-package stubs), records every generated migration path, migrates a fresh SQLite database, and starts the real Django application with the affected view and route byte-bound to their commit identities.
  4. For every target, it creates a regular Django User plus required Employee, asserts is_staff=false and is_superuser=false, and does not create a session during setup.
  5. A distinct attacker container GETs /login/, extracts CSRF, POSTs the actual login form, and obtains the session over HTTP. It then sends an authenticated in-root request, an authenticated traversal, and a no-cookie/no-Authorization traversal with Referer: http://attacker.invalid/login.
  6. One invocation runs two fresh vulnerable and two fresh fixed targets. The final script was executed twice consecutively; both bundle/logs/clean_execution_1.log and clean_execution_2.log end in the PASS oracle.

Evidence

  • bundle/logs/clean_execution_1.log and bundle/logs/clean_execution_2.log — two consecutive complete final-script passes.
  • bundle/logs/reproduction_steps.log — transcript of the latest complete execution.
  • bundle/logs/http-proof/source_identity.log — vulnerable, fixed, and patch commit identities.
  • bundle/logs/http-proof/modified_files.log — startup accommodation and modified-file inventory.
  • bundle/logs/http-proof/image_identity_{vulnerable,fixed}.log and dependency_identity_{vulnerable,fixed}.json — runtime launcher and tag-specific dependency identities.
  • bundle/logs/http-proof/run_{1,2}_{vulnerable,fixed}/runtime_binding.log — live target module paths and SHA-256 values for /app/base/views.py and /app/base/urls.py, plus Django version.
  • bundle/logs/http-proof/run_{1,2}_{vulnerable,fixed}/generated_migrations_inventory.log — startup-only generated migration paths.
  • bundle/logs/http-proof/run_{1,2}_{vulnerable,fixed}/provision_identity.logis_staff=false, is_superuser=false, active Employee relation, and no setup-created session.
  • bundle/logs/http-proof/run_{1,2}_{vulnerable,fixed}/request_*.json and response_*.json — scrubbed raw targets, header presence, statuses, digests, and exact-canary comparisons.
  • bundle/logs/http-proof/run_{1,2}_{vulnerable,fixed}/server.log — real Django startup and server-observed HTTP paths.
  • bundle/logs/http-proof/run_{1,2}_{vulnerable,fixed}/{target,attacker}_container_identity.log — distinct process/container and network evidence.
  • bundle/logs/http-proof/run_{1,2}_{vulnerable,fixed}/result.json — structured per-instance oracle.
  • bundle/logs/http-proof/matrix_summary.json — latest four-instance aggregate. It records two vulnerable 200 outside-canary reads and two fixed 404 controls with live 200 in-root controls.
  • bundle/repro/runtime_manifest.json — strict machine-readable production-path manifest.

Key current-run observations include:

vulnerable authenticated_escape:   status=200, equals_outside_canary=true
vulnerable unauthenticated_escape: status=200, equals_outside_canary=true,
                                   cookie_sent=false, authorization_sent=false
fixed authenticated_escape:        status=404, equals_outside_canary=false
fixed unauthenticated_escape:      status=404, equals_outside_canary=false
all four authenticated in-root controls: status=200, equals_inside_canary=true

The request capture states that the supplied target was emitted without URL normalization; server logs independently show /media/../../runtime/outside_.... Passwords and cookie values are redacted or omitted from public logs.

Recommendations / Next Steps

  • Upgrade to Horilla 1.6.0 or later.
  • On maintained branches, resolve user-controlled paths with safe_join() (or equivalent canonical containment), reject containment failures, and require a regular file before opening it.
  • Never grant access based on the Referer header. Use authenticated identity and a narrowly reviewed allowlist of genuinely public media prefixes.
  • Add end-to-end server tests for raw ../, encoded traversal variants, mixed separators where relevant, absolute paths, directory requests, and symlink-related containment cases.
  • Keep a positive in-root control in regression tests to ensure the patch fails closed without disabling legitimate media delivery.

Additional Notes

  • Idempotency: Confirmed. The final script ran twice consecutively. Every invocation creates fresh databases, networks, containers, users, passwords, and canaries. Images/dependency directories are reused only after exact revision or requirements.txt digest checks.
  • Horilla tags ship only __init__.py in application migration directories. The script runs the product's own manage.py makemigrations as a documented startup accommodation, inventories generated files, and verifies affected base/views.py and base/urls.py bytes against git before and from each live target.
  • The server is Django's real manage.py runserver 0.0.0.0:8000 --noreload; attacker requests cross a private Docker TCP network from a separate container.
  • The observed impact is bounded to exact random canaries. Results do not assert that a URL-normalizing reverse proxy would preserve the same raw path or that every sensitive file is reachable.

GHSA-x52c-5hrq-76pq 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.

Event 1/40
0:004:33
0:00
session startedgpt-5.6-sol · GHSA-X52C-5HRQ-76PQ · REPRO-20
0:10
0:10
0:49
0:50
web search
1:01
1:01
1:01
1:02
1:03
web search
1:24
1:24
1:24
1:25
web search
1:52
1:54
web search
2:16
2:16
2:16
2:17
web search
2:19
web search
2:20
web search
4:24
4:24
4:24
4:24
4:24
4:24
extract_facts
no facts extracted
4:27
4:27
supportclaim_contract
4:33
4:33
4:33

Artifacts and Evidence for GHSA-x52c-5hrq-76pq

Scripts, logs, diffs, and output captured during the reproduction.

bundle/repro/reproduction_steps.sh29.6 KB
bundle/repro/rca_report.md9.6 KB
bundle/repro/runtime_manifest.json3.7 KB
bundle/logs/clean_execution_1.log1.2 KB
bundle/logs/clean_execution_2.log1.2 KB
bundle/logs/reproduction_steps.log1.2 KB
bundle/logs/http-proof/matrix_summary.json4.5 KB
bundle/logs/http-proof/source_identity.log0.3 KB
bundle/logs/http-proof/modified_files.log0.5 KB
bundle/logs/http-proof/run_1_vulnerable/result.json1.3 KB
bundle/logs/http-proof/run_2_vulnerable/result.json1.3 KB
bundle/logs/http-proof/run_1_fixed/result.json1.3 KB
bundle/logs/http-proof/run_2_fixed/result.json1.3 KB
bundle/logs/http-proof/run_1_vulnerable/server.log1.0 KB
bundle/logs/http-proof/run_2_vulnerable/server.log1.0 KB
bundle/logs/http-proof/run_1_fixed/server.log1.2 KB
bundle/logs/http-proof/run_2_fixed/server.log1.2 KB
bundle/logs/http-proof/run_1_vulnerable/request_unauthenticated_escape.json0.3 KB
bundle/logs/http-proof/run_1_vulnerable/response_unauthenticated_escape.json0.9 KB
bundle/logs/http-proof/run_2_vulnerable/request_unauthenticated_escape.json0.3 KB
bundle/logs/http-proof/run_2_vulnerable/response_unauthenticated_escape.json0.9 KB
bundle/logs/http-proof/run_1_fixed/request_unauthenticated_escape.json0.3 KB
bundle/logs/http-proof/run_1_fixed/response_unauthenticated_escape.json0.8 KB
bundle/logs/http-proof/run_2_fixed/request_unauthenticated_escape.json0.3 KB
bundle/logs/http-proof/run_2_fixed/response_unauthenticated_escape.json0.8 KB
bundle/logs/http-proof/run_1_vulnerable/runtime_binding.log0.7 KB
bundle/logs/http-proof/run_2_vulnerable/runtime_binding.log0.7 KB
bundle/logs/http-proof/run_1_fixed/runtime_binding.log0.7 KB
bundle/logs/http-proof/run_2_fixed/runtime_binding.log0.7 KB
bundle/logs/http-proof/run_1_vulnerable/provision_identity.log0.5 KB
bundle/logs/http-proof/run_2_vulnerable/provision_identity.log0.5 KB
bundle/logs/http-proof/run_1_fixed/provision_identity.log0.5 KB
bundle/logs/http-proof/run_2_fixed/provision_identity.log0.5 KB
bundle/logs/http-proof/run_1_vulnerable/generated_migrations_inventory.log2.4 KB
bundle/logs/http-proof/run_2_vulnerable/generated_migrations_inventory.log2.4 KB
bundle/logs/http-proof/run_1_fixed/generated_migrations_inventory.log2.4 KB
bundle/logs/http-proof/run_2_fixed/generated_migrations_inventory.log2.4 KB
bundle/logs/http-proof/run_1_vulnerable/target_container_identity.log0.2 KB
bundle/logs/http-proof/run_1_vulnerable/attacker_container_identity.log0.2 KB
bundle/logs/http-proof/run_1_fixed/target_container_identity.log0.2 KB
bundle/logs/http-proof/run_1_fixed/attacker_container_identity.log0.2 KB
bundle/logs/http-proof/run_2_vulnerable/target_container_identity.log0.2 KB
bundle/logs/http-proof/run_2_vulnerable/attacker_container_identity.log0.2 KB
bundle/logs/http-proof/run_2_fixed/target_container_identity.log0.2 KB
bundle/logs/http-proof/run_2_fixed/attacker_container_identity.log0.2 KB
bundle/logs/http-proof/run_1_vulnerable/request_authenticated_escape.json0.4 KB
bundle/logs/http-proof/run_1_vulnerable/response_authenticated_escape.json0.9 KB
bundle/logs/http-proof/run_1_fixed/request_authenticated_escape.json0.4 KB
bundle/logs/http-proof/run_1_fixed/response_authenticated_escape.json0.8 KB
bundle/logs/http-proof/run_2_vulnerable/request_authenticated_escape.json0.4 KB
bundle/logs/http-proof/run_2_vulnerable/response_authenticated_escape.json0.9 KB
bundle/logs/http-proof/run_2_fixed/request_authenticated_escape.json0.4 KB
bundle/logs/http-proof/run_2_fixed/response_authenticated_escape.json0.8 KB
bundle/repro/validation_verdict.json0.8 KB
08 · How to Fix

How to Fix GHSA-x52c-5hrq-76pq

Upgrade horilla/horilla-hr · github to 1.6.0 or later.

Coming soon

Step-by-step mitigation and hardening guidance for GHSA-x52c-5hrq-76pq — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: GHSA-x52c-5hrq-76pq

Is GHSA-x52c-5hrq-76pq exploitable?

Yes. Pruva independently reproduced GHSA-x52c-5hrq-76pq in horilla/horilla-hr and verified the exploit fires end-to-end in a sandboxed environment. A runnable proof-of-concept script and the full agent transcript are on this page (reproduction REPRO-2026-00295).

How severe is GHSA-x52c-5hrq-76pq?

GHSA-x52c-5hrq-76pq is rated high severity, with a CVSS score of 7.5 out of 10.

What type of vulnerability is GHSA-x52c-5hrq-76pq?

GHSA-x52c-5hrq-76pq is classified as CWE-22 (Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')), a Path Traversal vulnerability.

Which versions of horilla/horilla-hr are affected by GHSA-x52c-5hrq-76pq?

horilla/horilla-hr >=1.3.1, <1.6.0 is affected by GHSA-x52c-5hrq-76pq.

Is there a fix for GHSA-x52c-5hrq-76pq?

Yes. GHSA-x52c-5hrq-76pq is fixed in horilla/horilla-hr 1.6.0. Upgrading to the fixed version remediates the issue.

How can I reproduce GHSA-x52c-5hrq-76pq?

Pruva provides a verified reproduction script on this page. Download it and run it inside an isolated environment such as a container or virtual machine — never against production. The reproduction was confirmed end-to-end by Pruva's automated agents.

Is the GHSA-x52c-5hrq-76pq reproduction verified?

Yes. Pruva reproduced GHSA-x52c-5hrq-76pq with high confidence in a sandboxed environment, capturing the full agent transcript and artifacts as evidence.
11 · References

References for GHSA-x52c-5hrq-76pq

Authoritative sources for GHSA-x52c-5hrq-76pq — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.