{"id":"CVE-2026-49342","title":"YARD static cache reads raw traversal paths before router sanitization","summary":"YARD static cache reads raw traversal paths before router sanitization","severity":"medium","cvss":5.3,"cwe":["CWE-22"],"vendor":"yard","product":"yard","ecosystem":"rubygems","affected":["yard < 0.9.44"],"patched":["yard 0.9.44"],"published":"2026-06-26","updated":"2026-06-26","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-pxcc-8665-phx8","references":[{"url":"https://github.com/lsegal/yard/security/advisories/GHSA-pxcc-8665-phx8"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-49342"},{"url":"https://github.com/lsegal/yard/commit/f78c19f0dd33a407085b4ed181bb60c0aa0078b4"},{"url":"https://github.com/advisories/GHSA-pxcc-8665-phx8"}],"tags":["ghsa","rubygems"],"epss":0.00403,"epssPercentile":0.34262,"ingestedAt":"2026-06-29T13:24:35.096Z","slug":"CVE-2026-49342","body":"## Overview\n\n### Summary\nYARD's static cache lookup reads a request path before the router's path cleanup runs. When a server is configured with a document root, a traversal path such as `/../yard-cache-secret.html` is joined against that root and can return a readable sibling `.html` file outside the intended static tree. \n\nThe potential security risk seems low, as only html-ending files can be read, but still the risk of reading arbitrary html files is a confiendtiality issue in itself, which is why we decided to report. Please let us know if this is out of your project's scope.\n\n### Details\nThe `--docroot` CLI option stores the configured directory in `server_options[:DocumentRoot]` at `lib/yard/cli/server.rb:198`, and adapter initialization copies that value into `adapter.document_root` at `lib/yard/server/adapter.rb:76`. For Rack requests, `RackAdapter#call` builds a request object from the Rack environment at `lib/yard/server/rack_adapter.rb:58` and passes it to `router.call(request)` at `lib/yard/server/rack_adapter.rb:60`. `Router#call` then stores the incoming request at `lib/yard/server/router.rb:55` and invokes `check_static_cache` before normal routing at `lib/yard/server/router.rb:56`. Inside `check_static_cache`, the only initial guard is that `adapter.document_root` is present at `lib/yard/server/static_caching.rb:35`; the cache path is built from `File.join(adapter.document_root, request.path.sub(/\\.html$/, '') + '.html')` at `lib/yard/server/static_caching.rb:36`, without cleaning `..` components first. If that resolved path is a regular file, `File.file?` accepts it at `lib/yard/server/static_caching.rb:38` and the file bytes are returned as a `200` HTML response at `lib/yard/server/static_caching.rb:40`. The later route sanitizer in `final_options` uses `File.cleanpath(...).gsub(...)` at `lib/yard/server/router.rb:181` and `lib/yard/server/router.rb:182`, but a static-cache hit returns before that code is reached.\n\n### PoC\n[poc.zip](https://github.com/user-attachments/files/28185421/poc.zip)\n\n```bash\nbash ./poc/run.sh\n```\nexpected output:\n```text\nrun 1: exit=0 timed_out=False duration=0.08s matched=True phase=oracle fingerprint='YARD_STATIC_CACHE_PATH_TRAVERSAL'\nrun 2: exit=0 timed_out=False duration=0.08s matched=True phase=oracle fingerprint='YARD_STATIC_CACHE_PATH_TRAVERSAL'\nrun 3: exit=0 timed_out=False duration=0.08s matched=True phase=oracle fingerprint='YARD_STATIC_CACHE_PATH_TRAVERSAL'\n```\n\nThe `YARD_STATIC_CACHE_PATH_TRAVERSAL` fingerprint is emitted only after the PoC observes a `200` static-cache response whose body contains the sibling file outside the configured document root. A setup failure, syntax failure, or cache miss would not print this oracle and would not demonstrate this traversal read.\n\n### Impact\nA remote unauthenticated HTTP client who can reach a YARD documentation server with `DocumentRoot`/`--docroot` enabled can request `.html` paths containing parent-directory components and receive readable matching files outside the configured document root. The required guards are narrow: `adapter.document_root` must be set, the traversed target must exist as a regular readable file, and the target must be reachable through the implementation's forced `.html` suffix. Those requests bypass the later `final_options` path cleanup because the cache check runs first. The resulting severity class is information disclosure: response bodies can contain off-root `.html` file contents, but this path does not show write access, code execution, or arbitrary files without the `.html` constraint.\n\n## Affected packages\n\n- `yard < 0.9.44`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `yard 0.9.44`","depth":"sunlit","depthScore":29,"depthScoreParts":{"impact":29.2,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}