fix(security): block Object.prototype filter/tag lookups (RCE)

`liquid.filters` and `liquid.tags` were plain `{}` so bracket access on
template-controlled keys inherited from `Object.prototype`. Most damaging:
`{{ x | valueOf }}` resolved to `Object.prototype.valueOf`, which the
filter pipeline called as a handler with `this = FilterImpl`; valueOf
returns its receiver, leaking `context`, `liquid`, `token` (and via them
parser, loader, fs) into the template — chain that with `group_by`/`where`
gadgets and an attacker reaches `Function`/`child_process` for RCE.
Same shape on the tag side: `{% constructor %}` bypassed the
"tag not found" assertion and crashed with a confusing message.

Use null-prototype storage so `liquid.filters[name]` / `liquid.tags[name]`
only resolve to explicitly registered entries. The existing
`assert(impl || !strictFilters)` and `assert(TagClass, ...)` now do the
right thing for `valueOf`, `toString`, `constructor`, `__proto__`,
`hasOwnProperty`, `isPrototypeOf`, `__defineGetter__`, etc.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Yang Jun
2026-05-12 00:11:47 +08:00
co-authored by Cursor
parent 3616a744b9
commit 09b12b7510
2 changed files with 44 additions and 2 deletions
+2 -2
View File
@@ -15,8 +15,8 @@ export class Liquid {
* @deprecated will be removed. In tags use `this.parser` instead
*/
public readonly parser: Parser
public readonly filters: Record<string, FilterImplOptions> = {}
public readonly tags: Record<string, TagClass> = {}
public readonly filters: Record<string, FilterImplOptions> = Object.create(null)
public readonly tags: Record<string, TagClass> = Object.create(null)
public constructor (opts: LiquidOptions = {}) {
this.options = normalize(opts)