GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-V273-448J-V4QJ

Vulnerability from github – Published: 2026-04-08 15:04 – Updated: 2026-04-09 14:29
VLAI
Summary
LiquidJS: `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.

The 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.

Root cause: - src/parser/parser.ts:83-85 calls loader.lookup(file, LookupType.Root, ...) and then reads the returned file. - src/fs/loader.ts:38 passes type !== LookupType.Root into candidates(). - For LookupType.Root, enforceRoot is false, so src/fs/loader.ts:47-66 accepts resolved absolute paths and fallback results without any contains() check.

This 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.

Proof of concept:

const fs = require('fs');
const os = require('os');
const path = require('path');
const { Liquid } = require('liquidjs');

const safeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'liquidjs-safe-root-'));
const engine = new Liquid({ root: [safeRoot], extname: '.liquid' });

engine.renderFile('/etc/hosts').then(console.log);

Expected result: a path outside root should be rejected. Actual result: /etc/hosts is rendered successfully.

Impact: 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.

Suggested 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.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 10.25.4"
      },
      "package": {
        "ecosystem": "npm",
        "name": "liquidjs"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "10.25.5"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-39859"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-08T15:04:44Z",
    "nvd_published_at": "2026-04-08T20:16:26Z",
    "severity": "MODERATE"
  },
  "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(\u0027/etc/hosts\u0027)` 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(\u0027fs\u0027);\nconst os = require(\u0027os\u0027);\nconst path = require(\u0027path\u0027);\nconst { Liquid } = require(\u0027liquidjs\u0027);\n\nconst safeRoot = fs.mkdtempSync(path.join(os.tmpdir(), \u0027liquidjs-safe-root-\u0027));\nconst engine = new Liquid({ root: [safeRoot], extname: \u0027.liquid\u0027 });\n\nengine.renderFile(\u0027/etc/hosts\u0027).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(\u0027/etc/hosts\u0027)` fails when `root` points to an unrelated directory.",
  "id": "GHSA-v273-448j-v4qj",
  "modified": "2026-04-09T14:29:19Z",
  "published": "2026-04-08T15:04:44Z",
  "references": [
    {
      "type": "WEB",
      "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "LiquidJS: `renderFile()` / `parseFile()` bypass configured `root` and allow arbitrary file read"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…