Restore null-prototype hardening for Jekyll include bindings, colocate blocked-key checks with readJSProperty, align ownPropertyOnly JSDoc with security docs, and drop integration tests duplicated in context.spec.
Co-authored-by: Cursor <[email protected]>
Use shouldBlockScopeKeyRead in findScope hasKey so inherited
constructor/__proto__/prototype do not falsely match environments.
Remove redundant globals hasKey check; globals remains the fallback scope.
Co-authored-by: Cursor <[email protected]>
Iteration is documented as an ownPropertyOnly exception; restore
isIterable/toEnumerable and document inherited Symbol.iterator behavior.
Co-authored-by: Cursor <[email protected]>
Block __proto__, constructor, and prototype only when ownPropertyOnly
is true or when access would traverse the prototype chain. Allow own
properties with those names when ownPropertyOnly is false.
Co-authored-by: Cursor <[email protected]>
Block writes to dangerous keys in assign/capture/increment/decrement, use own-property Symbol.iterator for plain objects when ownPropertyOnly is true, fix inherited size reads, and sanitize filter iteration scopes.
Co-authored-by: Cursor <[email protected]>
* feat: remove memoryLimit option (#910)
Co-authored-by: Cursor <[email protected]>
* feat: add templateLimit, outputLengthLimit, and maxDepth DoS limits
Enforce v11 resource guards in render and tags, fix for offset/else behavior, and update tutorials for Tag-class registration.
Co-authored-by: Cursor <[email protected]>
* docs: revert unnecessary tutorial churn from memoryLimit PR
Restore the two-example register-filters-tags structure (Value + Hash)
and undo unrelated constructor/emitter doc edits not required for DoS limits.
Co-authored-by: Cursor <[email protected]>
* docs: trim security-model prose and update render-tag-content
Remove diary-style engine comparisons from security-model.md.
Update render-tag-content tutorial to Tag class examples with tpls class field.
Co-authored-by: Cursor <[email protected]>
* docs: note maxDepth stack overflow applies to renderSync only
Explain why async render does not need maxDepth for stack protection based on generator/toPromise driving.
Co-authored-by: Cursor <[email protected]>
* refactor: track maxDepth via depthLimit Limiter on Context
Replace increaseDepth/decreaseDepth with a shared Limiter that supports
paired use/release, matching templateLimit and outputLengthLimit patterns.
Co-authored-by: Cursor <[email protected]>
* fix: remove spurious diff noise in filter files
Restore misc.ts from origin/next with LF line endings and re-apply only
memoryLimit removal, avoiding CRLF and blank-line churn in the export block.
Co-authored-by: Cursor <[email protected]>
* refactor: minimize PR diff noise
Co-authored-by: Cursor <[email protected]>
* feat: cap strftime pad width at 1M
docs: restructure security model with production guidance
Co-authored-by: Cursor <[email protected]>
* refactor: simplify depthLimit in partial tags and tighten security docs
Drop try/finally around depthLimit in include, layout, and render; release at generator end. Consolidate production guidance in security-model.md. Fix padded-blocks lint in dos.spec.ts.
Co-authored-by: Cursor <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
Remove KeepingTypeEmitter and always stringify via SimpleEmitter. On next, map breaking commits to patch so alpha stays on 11.x.
Co-authored-by: Cursor <[email protected]>
* feat!: drop TagImplOptions in favor of Tag classes (#839)
Remove tag-options-adapter and the registerTag object-literal overload.
Custom tags must extend Tag.
Co-authored-by: Cursor <[email protected]>
* test: drop TagImplOptions-specific e2e coverage (#839)
Remove #570 v9 object-literal registration test and unused metadata_file setup in #573.
Co-authored-by: Cursor <[email protected]>
* test: use inline Tag classes in register-tags spec
Co-authored-by: Cursor <[email protected]>
* test: remove dead throwingTag setup and duplicate throw stub
for.spec kept throwingTag registration after #713 removed its test. Reuse ThrowingTag in liquid.spec instead of IntendedRenderErrorTag.
Co-authored-by: Cursor <[email protected]>
* test: remove dead throwingTag setup and duplicate throw stub
for.spec kept throwingTag registration after #713 removed its test. Reuse ThrowingTag in liquid.spec instead of IntendedRenderErrorTag.
Co-authored-by: Cursor <[email protected]>
* fix(demo): ignore killall exit when express server already stopped
Co-authored-by: Cursor <[email protected]>
* fix(demo): revert unrelated return->exit change in express test
Co-authored-by: Cursor <[email protected]>
* fix(demo): use exit in express test script (no enclosing function)
Co-authored-by: Cursor <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
* fix(filters): charge join/array_to_sentence_string by output size
join charged memoryLimit by array element count, not by the string it
produces, letting concat doubling (cheap reference copies) inflate an
array's element count and then materialize a huge string via join far
past the configured memoryLimit (GHSA-4r6h-5v86-94p3). Charge by the
sum of stringified element lengths plus separators before allocating.
Apply the same fix to the sibling array_to_sentence_string filter.
Co-authored-by: Cursor <[email protected]>
* refactor(filters): simplify join output-size accounting
Sum stringified element lengths in a single pass and keep the guarded
Array.prototype.join for the result, instead of building an intermediate
parts array.
Co-authored-by: Cursor <[email protected]>
* fix(filters): charge json/jsonify/inspect serialization to memoryLimit
json/jsonify/inspect serialized values without charging memoryLimit, so
a concat-doubled array (cheap reference copies) could be materialized
into a huge JSON string past the configured limit — the same unbounded
class as the join bug (GHSA-4r6h-5v86-94p3). Charge via a JSON.stringify
replacer that accounts string lengths as it walks, aborting mid-
serialization instead of allocating the full blob first.
Co-authored-by: Cursor <[email protected]>
* fix(memory): charge rendered output to memoryLimit at emission
Move output-length accounting into the emitters, which charge each
written chunk against ctx.memoryLimit right before it reaches the
result string or stream. Filters/tags now only pre-charge the extra
working memory they allocate apart from that output, so join drops its
bespoke output-size counting and charges array.length like its siblings.
The block.super capture emitter intentionally omits the limiter to
avoid double-counting content that is re-emitted through the final
emitter.
Co-authored-by: Cursor <[email protected]>
* refactor(filters): rely on emitter output charge for json/inspect/array_to_sentence_string
With rendered output charged at emission, these filters no longer need
bespoke output-size counting: the emitted case is covered by the final
emitter. Revert json/inspect to their original form and array_to_sentence_string
to its element-count charge, dropping the non-emitted `| size` guards.
Co-authored-by: Cursor <[email protected]>
* revert(memory): drop emitter output charge, restore filter output-size accounting
join/array_to_sentence_string/json/inspect charge memoryLimit by the
string they materialize (not element count), so discarded results like
{% assign out = a | join %}{{ out | size }} are still bounded.
Remove the emitter-level limiter added in 2f343f063; it cannot catch
materialized-but-not-emitted values.
Co-authored-by: Cursor <[email protected]>
* fix(filters): charge json/inspect replacer by serialized node size
Replace the flat 1-unit charge for non-string JSON nodes with per-type
estimates (primitives via JSON.stringify length, containers by structure).
Co-authored-by: Cursor <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
* fix(security): charge pop filter allocation to memoryLimit (CWE-770)
The `pop` array filter cloned the input via `[...toArray(v)]` without
charging `this.context.memoryLimit.use(...)`, bypassing the memoryLimit
DoS guard that its sibling filters (shift, unshift, compact, etc.) apply.
Mirror `shift` to account for the O(N) allocation.
Co-authored-by: Cursor <[email protected]>
* fix(security): charge sample filter full clone allocation to memoryLimit (CWE-770)
Co-authored-by: Cursor <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
* docs: add GitHub buttons and improve option docs
* chore: replace husky with prepush check
* docs: revamp homepage and switch to custom GitHub buttons
- Make the docs English-only by removing all zh-cn content, the language switcher UI, and related JS/config
- Rework homepage feature cards (Safe & Typed, Pure JavaScript, Shopify & Jekyll, Streaming) and refresh section colors/layout
- Replace buttons.github.io with custom Star/Sponsor buttons featuring a live star count and dark-mode support
- Drop the buttons.js script and tidy banner, header, footer, and share partials
Co-authored-by: Cursor <[email protected]>
* fix: restore tsconfig settings and changelog build
Re-add suppressImplicitAnyIndexErrors and downlevelIteration removed in
a75033e2c, which broke the rollup TypeScript build on CI. Drop zh-cn
changelog output now that translations were removed.
Co-authored-by: Cursor <[email protected]>
* fix: resolve TS errors without deprecated tsconfig options
Co-authored-by: Cursor <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
* 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]>
* test: fold prototype-registry regressions into register + e2e
Co-authored-by: Cursor <[email protected]>
* test: assert null-prototype registries vs all Object.prototype keys
Co-authored-by: Cursor <[email protected]>
* test: dedupe registry checks; merge filter prototype loop
Co-authored-by: Cursor <[email protected]>
* fix(context): use null-prototype scope and register objects
Add createScope(); use for bottom scope, spawn default, getAll merge, ctx.push frames, filter loops, include/layout blocks registers, and cycle groups. registers uses Object.create(null) and getRegister uses ??.
For-loop continue register defaults to 0 (not {}): Array.slice coerces plain {} but not null-prototype objects.
Export createScope from the package entry.
Co-authored-by: Cursor <[email protected]>
* revert(context): plain {} registers and getRegister ||
Registers are only mutated by tag implementations, not templates; keep null-prototype scopes/createScope for push frames.
Co-authored-by: Cursor <[email protected]>
* test(context): assert scope isolation without probing prototypes
Replace Object.getPrototypeOf checks for bottom() and getAll() with
'in' checks on typical Object.prototype names plus a merge assertion.
Co-authored-by: Cursor <[email protected]>
* test(e2e): assert constructor filter/tag lookups (node + UMD)
Co-authored-by: Cursor <[email protected]>
* test(context): cover Object.prototype keys under ownPropertyOnly
- Add getSync cases for constructor and valueOf on plain objects
- Remove scope storage tests that used the in operator
Co-authored-by: Cursor <[email protected]>
* refactor: remove createScope helper
Drop the exported helper and finish migrating call sites. Revert incidental context/for/include/layout churn so behavior matches mainline aside from the removal. Trim duplicate e2e and heavy Object.prototype loops in registry tests.
Co-authored-by: Cursor <[email protected]>
* docs: document ownPropertyOnly and Drop security in security model
Co-authored-by: Cursor <[email protected]>
* docs(zh-cn): sync security model with ownPropertyOnly and Drop notes
Co-authored-by: Cursor <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
* fix(strip_html): rewrite as linear single-pass scan to avoid ReDoS
The previous strip_html regex
/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<[\s\S]*?>|<!--[\s\S]*?-->/g
contains lazy alternatives that backtrack O(n^2) on inputs with many
unclosed `<script` / `<style` openers. A 350KB payload of
`'<script'.repeat(50000)` blocked the Node.js event loop for ~10s, and
cost grew quadratically with input size. memoryLimit only charged
str.length, which does not bound regex CPU.
Replace the regex with an indexOf-based single-pass scan. For each `<`
we:
- if `<script` opener: find next `</script>` and skip the whole block;
cache "no closer after pos k" so subsequent unclosed `<script`
openers do not re-scan the tail.
- same for `<style` / `</style>`.
- otherwise treat as a generic `<...>` tag (matches the original
behavior, where the `<[\s\S]*?>` alternative also caught comments).
- if no closing `>` exists, emit the tail as literal text and stop.
Total work is O(n). All existing strip_html test cases pass unchanged.
Add regression tests covering the PoCs (`<script` / `<style` repeats,
and `<script>foo` repeats with `>` but no `</script>`) plus a
memoryLimit assertion.
Co-authored-by: Cursor <[email protected]>
* refactor(strip_html): factor block kinds into a small table
Same algorithm and complexity, fewer lines. Document why a regex-only
solution can't be O(n) in V8 (no atomic groups / possessive quantifiers
/ memoization, so unrolled-loop patterns are still O(n^2) on unclosed
openers — empirically confirmed: original 280KB ~4s, Friedl unrolled
~14s, atomic lookahead ~7s; tokenizer ~1ms).
Co-authored-by: Cursor <[email protected]>
* refactor(strip_html): inline block kinds to match file style
Drop the module-level STRIP_BLOCKS table; the rest of the file keeps
each filter self-contained (only escapeMap/unescapeMap are top-level
maps shared across filters). Two openers don't justify a table.
Co-authored-by: Cursor <[email protected]>
* refactor(strip_html): unify raw-text blocks; treat <!--...--> as opaque
In HTML5, <script>, <style>, and <!-- --> are all raw-text blocks: their
content is opaque until the matching closer, so a `>` inside CSS, JS, or
a comment must not be treated as a tag end. The previous code only had
this special handling for <script> and <style>; comments containing `>`
fell through to the generic `<...>` branch and were partially stripped
(e.g. `<!-- a > b -->` left `b -->` in the output).
Match Shopify Liquid's STRIP_HTML_BLOCKS set (script + style + comment),
and consolidate the three near-identical branches into a small
opener/closer table inside the function.
Algorithm and complexity unchanged (O(n) via indexOf + cached closer
positions). Add a regression test for `>` inside a comment.
Co-authored-by: Cursor <[email protected]>
* refactor(strip_html): drop position cache, delete dead blocks from Set
Once `indexOf(closer, X)` returns -1, all subsequent searches (with
monotonically increasing start) also return -1. So tracking absence is
enough; storing positions is unnecessary. Make `blocks` a Set and
delete a kind once its closer is known absent — no parallel `dead`
bookkeeping. Use Jest's per-test timeout for the ReDoS regressions
instead of manual Date.now() bookkeeping.
Co-authored-by: Cursor <[email protected]>
* refactor(strip_html): treat '<...>' as a catch-all block kind
Adding ['<', '>'] as the lowest-priority entry of `blocks` lets the
inner loop subsume the generic-tag fallback: the `end` sentinel and
its `< 0` / `<= 0` follow-up checks disappear, the "no terminator"
exit becomes a single `i === lt` test, and Set<[string, string]>
collapses to Map<string, string>.
Co-authored-by: Cursor <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
renderLimit was only checked inside the per-template loop, so
renderTemplates([], ...) skipped it. Empty {% for %} and {% tablerow %}
bodies call that path once per iteration (tablerow still does emitter
work for <tr>/<td>), bypassing the documented time budget. Check the
limiter at renderTemplates entry before the loop.
Add regression tests for empty for-body and empty tablerow-body.
Co-authored-by: Cursor <[email protected]>
* feat: static variable analysis
* Accept any iterable from `children`, `arguments`, etc.
* Test analysis of standard tags
* Use `TagToken.tokenizer` instead of creating a new one
* Test analysis of netsted tags
* Group variables by their root value
* Test analysis of nested globals and locals
* Analyze included and rendered templates WIP
* Use existing tokenizer when constructing `Hash`
* Improve test coverage
* Analyze variables from `layout` and `block` tags
* Test analysis of Jekyll style includes
* Handle variables that start with a nested variable
* Async analysis
* Test non-standard tag end to end
* Implement convenience analysis methods on the `Liquid` class
* More analysis convenience methods
* Accept string or template array
* Draft static analysis docs
* Deduplicate variables names
* Fix isolated scope global variable map
* Coerce variables to strings instead of extending String
* Private map instead of extending Map
* Fix e2e test
* Tentatively implement analysis of aliased variables
* Fix nested variable segments array
* Update docs sidebar
- `relativeReference` is enabled by default, set to `false` to disable
- Referenced files are still constrained within root/partias/layouts
- fix: relative filenames are not constrained (which allows arbitrary filesystem read)
Example Usage:
{% render "../foo/bar.html" %}
Note:
../foo/bar.html' should also be within `partials` (or `root` if `partials` not set)
- previously deprecated `getTemplate()` and `getTemplateSync()` not no longer supported
- `opts` no longer support dynamic set in `parseFile()`, `renderFile()` arguments