A circular block reference in {% layout %} / {% block %} causes an infinite recursive loop, consuming all available memory (~4GB) and crashing the Node.js process with FATAL ERROR: JavaScript heap out of memory. This allows any user who can submit a Liquid template to perform a Denial of Service attack.
LiquidJS enforces partial and layout root restrictions using the resolved pathname string, but it does not resolve the canonical filesystem path before opening the file. A symlink placed inside an allowed partials or layouts directory can therefore point to a file outside that directory and still be loaded.
The sort_natural filter bypasses the ownPropertyOnly security option, allowing template authors to extract values of prototype-inherited properties through a sorting side-channel attack. Applications relying on ownPropertyOnly: true as a security boundary (e.g., multi-tenant template systems) are exposed to information disclosure of sensitive prototype properties such as API keys and tokens.
liquidjs 10.25.0 documents root as constraining filenames passed to renderFile() and parseFile(), but top-level file loads do not enforce that boundary. The published npm package liquidjs@10.25.0 on Linux 6.17.0 with Node v22.22.1. A Liquid instance configured with an empty temporary directory as root still returned the contents of /etc/hosts when renderFile('/etc/hosts') was called. I have not exhaustively checked older releases yet; 10.25.0 is the latest tested version. Root cause: src/parser/parser.ts:83-85 …
The replace filter in LiquidJS incorrectly accounts for memory usage when the memoryLimit option is enabled. It charges str.length + pattern.length + replacement.length bytes to the memory limiter, but the actual output from str.split(pattern).join(replacement) can be quadratically larger when the pattern occurs many times in the input string. This allows an attacker who controls template content to bypass the memoryLimit DoS protection with approximately 2,500x amplification, potentially causing out-of-memory conditions.
LiquidJS's memoryLimit security mechanism can be completely bypassed by using reverse range expressions (e.g., (100000000..1)), allowing an attacker to allocate unlimited memory. Combined with a string flattening operation (e.g., replace filter), this causes a V8 Fatal error that crashes the Node.js process, resulting in complete denial of service from a single HTTP request.
The replace_first filter in LiquidJS uses JavaScript's String.prototype.replace() which interprets $& as a backreference to the matched substring. The filter only charges memoryLimit for the input string length, not the amplified output. An attacker can achieve exponential memory amplification (up to 625,000:1) while staying within the memoryLimit budget, leading to denial of service.
The layout, render, and include tags allow arbitrary file access via absolute paths (either as string literals or through Liquid variables, the latter require dynamicPartials: true, which is the default). This poses a security risk when malicious users are allowed to control the template content or specify the filepath to be included as a Liquid variable.