{"id":"CVE-2026-39859","aliases":["GHSA-v273-448j-v4qj"],"url":"https://o3.security/vulnerability/CVE-2026-39859","summary":"LiquidJS has a renderFile() / parseFile() bypass configured root and allow arbitrary file read","details":"`liquidjs` 10.25.0 documents `root` as constraining filenames passed to `renderFile()` and `parseFile()`, but top-level file loads do not enforce that boundary.\n\nThe published npm package `liquidjs@10.25.0` on Linux 6.17.0 with Node v22.22.1. A `Liquid` instance configured with an empty temporary directory as `root` still returned the contents of `/etc/hosts` when `renderFile('/etc/hosts')` was called. I have not exhaustively checked older releases yet; 10.25.0 is the latest tested version.\n\nRoot cause:\n- `src/parser/parser.ts:83-85` calls `loader.lookup(file, LookupType.Root, ...)` and then reads the returned file.\n- `src/fs/loader.ts:38` passes `type !== LookupType.Root` into `candidates()`.\n- For `LookupType.Root`, `enforceRoot` is false, so `src/fs/loader.ts:47-66` accepts resolved absolute paths and fallback results without any `contains()` check.\n\nThis appears adjacent to the March 10, 2026 fix for CVE-2026-30952, which hardened `include` / `render` / `layout` but not the top-level file-loading APIs.\n\nProof of concept:\n```javascript\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { Liquid } = require('liquidjs');\n\nconst safeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'liquidjs-safe-root-'));\nconst engine = new Liquid({ root: [safeRoot], extname: '.liquid' });\n\nengine.renderFile('/etc/hosts').then(console.log);\n```\n\nExpected result: a path outside `root` should be rejected.\nActual result: `/etc/hosts` is rendered successfully.\n\nImpact: any application that treats `root` as a sandbox boundary and forwards attacker-controlled template names into `renderFile()` or `parseFile()` can disclose arbitrary local files readable by the server process.\n\nSuggested fix: apply the same containment checks used for partial/layout lookups to `LookupType.Root`, and reject absolute or fallback paths unless they remain within an allowed root. A regression test should verify that `renderFile('/etc/hosts')` fails when `root` points to an unrelated directory.","published":"2026-04-08T19:45:21.747Z","modified":"2026-10-28T02:31:06.076689465Z","cvss":null,"epss":{"score":0.00447,"percentile":0.38124,"asOf":"2026-09-17"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"liquidjs","fixedVersion":"10.25.5"}],"fix":{"url":"https://github.com/harttle/liquidjs/pull/870","label":"harttle/liquidjs#870"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/39xxx/CVE-2026-39859.json"},{"type":"ADVISORY","url":"https://github.com/harttle/liquidjs/security/advisories/GHSA-v273-448j-v4qj"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-39859"},{"type":"WEB","url":"https://github.com/harttle/liquidjs/pull/870"},{"type":"WEB","url":"https://github.com/harttle/liquidjs/commit/f41c1fc02fe901598f3328118b42b13bc6bc9b04"},{"type":"PACKAGE","url":"https://github.com/harttle/liquidjs"},{"type":"WEB","url":"https://github.com/harttle/liquidjs/releases/tag/v10.25.5"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-28T02:31:06.076689465Z"}}