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 Variant found 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://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

Reproduced
  • reached the target end-to-end
  • on the real production code path
Variants tested

Unauthenticated /media/base/icon/../../<private-file> is classified by the raw public prefix but opened at the normalized private MEDIA_ROOT target, bypassing protected-media authentication on fixed and current Horilla revisions.

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.

Variant Analysis & Alternative Triggers for GHSA-x52c-5hrq-76pq

Fixed: tag 1.6.0, commit b3bd29d15819cbece45c58e6268ddd0614e387d6;

A distinct authorization bypass remains in Horilla's real GET /media/<path> endpoint after the path-containment fix. protected_media() decides that a request is public by testing the raw route string with path.startswith("base/icon/") (and, on current branches, two additional public prefixes), but it opens the normalized filesystem path returned by safe_join(). An unauthenticated requester can therefore ask for /media/base/icon/../../private_<canary>.txt: safe_join() correctly keeps the result inside MEDIA_ROOT, while normalization resolves the filename to MEDIA_ROOT/private_<canary>.txt; the raw string still starts with the public prefix, so authentication is skipped and the private media canary is returned. The final script confirmed this on fixed tag 1.6.0, current 1.x, current v2, and the older release/v2.1.0 sibling. It also confirmed that plain outside-root traversal is blocked on fixed/current branches. This is an authorization bypass inside MEDIA_ROOT, not a bypass of safe_join()'s outside-root containment invariant.

Fix Coverage / Assumptions

The original containment fix, commit 67ac2056813ee95d4c4a0bfe7c0124a361cb6c48, changes base.views.protected_media from os.path.join(settings.MEDIA_ROOT, path) to safe_join(settings.MEDIA_ROOT, path), catches rejected paths as HTTP 404, and adds os.path.isfile(). Its correct invariant is: the normalized absolute filesystem target must stay beneath MEDIA_ROOT and be a regular file.

That fix explicitly covers:

  • plain outside-root .. traversal;
  • absolute-path escape attempts;
  • directory requests; and
  • the sole file-open sink in protected_media().

The fix assumes that containment and authorization are independent and that prefix authorization can safely inspect the unnormalized path argument. That assumption is false. A path can remain contained under MEDIA_ROOT while normalizing from an allowlisted public lexical prefix to a different, non-public subtree. The patch does not canonicalize the relative path before startswith() authorization, require a path-component boundary after canonicalization, or verify that the final resolved target remains beneath the selected public subtree.

Later 1.x/current-v2 code removes Referer-based authorization and uses public_media_prefixes, but preserves the same raw-string-prefix assumption. Thus it closes the parent public Referer composition while leaving this separate unauthenticated media authorization bypass.

The repository's SECURITY.md was read for each tested lineage. It treats authentication/authorization and input validation as security concerns and contains no exclusion that would make an unauthenticated private-media read expected behavior.

Variant / Alternate Trigger

Confirmed candidate C3-public-prefix-normalization:

  • Entrypoint: real Horilla HTTP route GET /media/<path> (base/urls.py, re_path(r"^media/(?P<path>.*)$", views.protected_media, ...)).
  • Request: GET /media/base/icon/../../private_<random>.txt with no Cookie and no Authorization header.
  • Data path: Django route capture → base.views.protected_media(request, path)safe_join(MEDIA_ROOT, path) → raw path.startswith(public_prefix) authorization → FileResponse(open(media_path, "rb")).
  • Distinctness: unlike the parent, this request does not escape MEDIA_ROOT and does not need the Referer bypass. It crosses the same remote HTTP trust boundary and reaches the same protected-media file-open sink, but exploits inconsistent representations between authorization (raw path) and file selection (normalized path).

The script also tested:

  1. C1-newer-2x-release-line-sibling: ordinary outside-root traversal through the newer 2.x release-line implementation. It reproduced on submitted 1.5.0 and old release/v2.1.0, but fixed 1.6.0 and both current branches returned 404. This records a fixed-line regression/sibling history, but it is not the primary current bypass.
  2. C2-double-percent-encoded-traversal: all tested targets returned 404; there is no second decode after safe_join() in this runtime path.
  3. C3-public-prefix-normalization: all tested targets returned HTTP 200 with the exact private in-root canary, including fixed/current targets; this is the confirmed bypass.
  • Component: Horilla HRMS base.views.protected_media and the real /media/<path> route.
  • Confirmed affected targets:
    • fixed tag 1.6.0, commit b3bd29d15819cbece45c58e6268ddd0614e387d6;
    • current 1.x/master, commit 11c4e3a2596c58f2381bda4c6bbc319a4430b097;
    • release/v2.1.0, commit b8cbf0af11d105e6e8c3708c3156322248f93e77;
    • current dev/v2.0, commit a971ff98662904f798819415203ccb26bc4b0db3.
  • Risk: unauthenticated information disclosure of a process-readable regular file under MEDIA_ROOT when its relative location is known or guessable.
  • Boundary: the demonstrated file is a fresh harmless canary at the root of MEDIA_ROOT. No secret, database, configuration file, /proc entry, or credential was read.

Impact Parity

  • Parent claimed maximum impact: unauthenticated remote read outside MEDIA_ROOT by composing path traversal and Referer authorization bypass.
  • Variant reproduced impact: unauthenticated remote read of an exact private canary inside MEDIA_ROOT, without Referer, cookie, bearer token, or session.
  • Parity: partial. The confidentiality class and remote unauthenticated trust boundary match, but this variant does not escape MEDIA_ROOT.
  • Not demonstrated: outside-root read on fixed/current branches, arbitrary-file discovery, account takeover, file write, code execution, session forgery, or access through every reverse proxy.

Root Cause

The same endpoint uses two representations of one attacker-controlled path:

media_path = safe_join(settings.MEDIA_ROOT, path)  # normalized target
...
is_public_asset = any(path.startswith(prefix) for prefix in public_media_prefixes)
...
return FileResponse(open(media_path, "rb"))

For path == "base/icon/../../private.txt", filesystem normalization produces MEDIA_ROOT/private.txt, which is still contained and therefore accepted by safe_join(). Authorization nevertheless sees the raw string beginning with base/icon/ and classifies it as public. The final open uses the normalized target, so a private file is served without authentication. Tag 1.6.0 uses the equivalent single-prefix test (exempted_folders) after the same safe_join() call.

This is root-cause-equivalent to the submitted issue at the sink and trust boundary: an untrusted route path controls the file selected by protected_media() and the endpoint opens it without enforcing the intended security boundary. The missed boundary here is the public-media subtree rather than MEDIA_ROOT itself.

Known containment fix: 67ac2056813ee95d4c4a0bfe7c0124a361cb6c48.

Reproduction Steps

  1. Run PRUVA_ROOT=/path/to/bundle bundle/vuln_variant/reproduction_steps.sh from any directory.
  2. The script resolves exact source identities, materializes immutable git archives, reads each target's SECURITY.md, starts the real Django WSGI route in a target process, and sends raw HTTP requests from a distinct attacker process.
  3. It runs submitted 1.5.0 first, patched 1.6.0 second, then release/v2.1.0, current 1.x/master, and current v2.
  4. Each target gets fresh inside, outside, and private-in-root canaries. The script tests the same plain traversal, double-encoded traversal, public-prefix normalization request, and a positive public in-root control.
  5. Expected success is exit 0 and the final line: PASS: public-prefix normalization bypass reads private media on fixed 1.6.0 and both current branches.

Evidence

  • bundle/logs/vuln_variant/reproduction_steps.log — final complete execution transcript.
  • bundle/logs/vuln_variant/matrix_summary.json — structured five-target, three-candidate result.
  • bundle/logs/vuln_variant/source_identity.log, fixed_version.txt, and latest_version.txt — exact submitted, fixed, release, and latest branch revisions.
  • bundle/logs/vuln_variant/candidate_matrix.json — source inspection and bounded candidate matrix.
  • bundle/logs/vuln_variant/security_policy_{submitted,fixed,newer_release,latest_v1,latest_v2}.md — threat-model/security-policy copies read during the run.
  • bundle/logs/vuln_variant/runtime_<role>/request_*.json and response_*.json — scrubbed raw request targets and exact-canary comparisons.
  • bundle/logs/vuln_variant/runtime_<role>/result.json — per-target runtime oracle.
  • bundle/logs/vuln_variant/runtime_<role>/server.log — live Django access logs preserving traversal targets.
  • bundle/logs/vuln_variant/runtime_<role>/runtime_binding.json — exact base/views.py and base/urls.py paths/hashes and runtime identity.

Key final observations:

plain outside-root traversal:
  submitted 1.5.0=200; release/v2.1.0=200
  fixed 1.6.0=404; latest 1.x=404; latest v2=404

public-prefix normalization private-media read:
  submitted=200; fixed 1.6.0=200; release/v2.1.0=200;
  latest 1.x=200; latest v2=200
  every body_equals_private_canary=true

positive /media/base/icon/<inside-canary> controls:
  all targets=200 and body_equals_inside_canary=true

Recommendations / Next Steps

  • Perform authorization against a single canonical relative path, not the raw route string.
  • For every public prefix, compute a canonical public directory with safe_join(MEDIA_ROOT, prefix) and require the canonical target to remain beneath that directory using path-aware containment (os.path.commonpath() or equivalent), including a component boundary.
  • Reject raw or decoded dot segments before authorization as defense in depth, but do not rely on string filtering alone.
  • Prefer explicit storage namespaces or a dedicated public-media endpoint/storage backend rather than mixing public and private files behind one route.
  • Resolve symlinks or open relative to a directory descriptor if untrusted users can influence filesystem links; safe_join() is lexical containment and does not itself enforce realpath containment.
  • Add end-to-end tests for each public prefix with prefix/../private, prefix/../../private, duplicate separators, percent encodings, and symlink targets, while retaining positive public and authenticated-private controls.

Additional Notes

  • Idempotency: confirmed. The final stage script completed successfully twice consecutively with fresh random canaries and ports.
  • The final runtime used Horilla's real URLconf, exact target base/urls.py, exact target base/views.py, and Django WSGI server. Source hashes are bound in each runtime_binding.json.
  • The Docker daemon became unavailable after the reproduction stage. To preserve fresh runtime validation, the stage used distinct local target/attacker processes over TCP. Optional ABI-bound document/data/image imports were stubbed only to accommodate the worker's Python ABI; affected URL/view files were not modified and are hash-bound to git. The v2 dependency cache advertised Django 4.2.24 although v2 requires 5.2, so a narrowly scoped Django compatibility namespace reports 5.2 and supplies the unchanged safe_join semantics needed by current v2. This limitation does not alter the vulnerable raw-prefix/open logic, but production-container confirmation remains useful before release.
  • C3's path remains within MEDIA_ROOT; it must not be described as arbitrary host filesystem traversal.

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/vuln_variant/reproduction_steps.sh13.4 KB
bundle/vuln_variant/local_http_runtime.py3.9 KB
bundle/vuln_variant/artifacts/http_variant_attacker.py3.4 KB
bundle/vuln_variant/compat/distutils/version.py0.3 KB
bundle/vuln_variant/compat/distutils/util.py0.2 KB
bundle/vuln_variant/compat/distutils/__init__.py0.0 KB
bundle/vuln_variant/django_compat/django/__init__.py0.6 KB
bundle/vuln_variant/django_compat/django/utils/__init__.py0.1 KB
bundle/vuln_variant/django_compat/django/utils/_os.py0.9 KB
bundle/vuln_variant/variant_manifest.json3.1 KB
bundle/vuln_variant/validation_verdict.json2.6 KB
bundle/vuln_variant/source_identity.json1.8 KB
bundle/vuln_variant/runtime_manifest.json2.9 KB
bundle/vuln_variant/root_cause_equivalence.json1.5 KB
bundle/vuln_variant/rca_report.md11.4 KB
bundle/vuln_variant/patch_analysis.md6.6 KB
bundle/logs/vuln_variant/reproduction_steps.log0.8 KB
bundle/logs/vuln_variant/matrix_summary.json1.5 KB
bundle/logs/vuln_variant/runtime_fixed/result.json1.8 KB
bundle/logs/vuln_variant/runtime_latest_v1/result.json1.9 KB
bundle/logs/vuln_variant/runtime_latest_v2/result.json1.9 KB
bundle/logs/vuln_variant/runtime_fixed/runtime_binding.json0.8 KB
bundle/logs/vuln_variant/runtime_latest_v1/runtime_binding.json0.9 KB
bundle/logs/vuln_variant/runtime_latest_v2/runtime_binding.json0.9 KB
bundle/logs/vuln_variant/runtime_fixed/request_public_prefix_normalization.json0.3 KB
bundle/logs/vuln_variant/runtime_fixed/response_public_prefix_normalization.json0.3 KB
bundle/logs/vuln_variant/source_identity.log0.5 KB
bundle/logs/vuln_variant/candidate_matrix.json2.0 KB
bundle/logs/vuln_variant/runtime_submitted/result.json1.9 KB
bundle/logs/vuln_variant/runtime_newer_release/result.json1.9 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.