Files
liquidjs/docs/source/tutorials/security-model.md
T
61ed163821 feat: remove memoryLimit; add templateLimit, outputLengthLimit, maxDepth (#937)
* 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]>
2026-07-15 22:58:13 +08:00

5.3 KiB

title
title
Security Model

LiquidJS provides DoS-oriented limits (parseLimit, templateLimit, outputLengthLimit, maxDepth) to reduce risk. This page summarizes those limits, ownPropertyOnly, custom Drop usage, and the security boundary to assume in production.

At a glance

LiquidJS ships a thin cooperative DoS layer:

  • parseLimit: limit total template size per parse() call.
  • templateLimit: limit total tag/HTML/output nodes rendered per render() call.
  • outputLengthLimit: limit total output length per render() call.
  • maxDepth: limit nesting depth of {% render %}, {% include %}, and {% layout %}.
  • Strftime numeric pad widths in the date filter are capped at 1_000_000 (1M) per conversion.

These are cooperative safeguards, not runtime isolation—see Production guidance below for host-level limits and online-service hardening.

Limit details

parseLimit

parseLimit restricts the size (character length) of templates parsed in each .parse() call, including referenced partials and layouts. Since LiquidJS parses template strings in near O(n) time, limiting total template length is usually sufficient.

A typical PC handles 1e8 (100M) characters without issues.

templateLimit

Restricting template size alone is insufficient because dynamic loops with large counts can occur during rendering. templateLimit mitigates this by limiting the number of tag, HTML literal, and output nodes rendered in each render() call.

{%- for i in (1..10000000) -%}
    order: {{i}}
{%- endfor -%}

Each template node (the for tag, literal order: , output {{i}}, and so on) counts toward the limit. In the above example, a limit of 30000000 would be exceeded before the loop finishes.

templateLimit is checked before each node render, so compute-intensive filters/tags/user-defined functions between checks can still cause DoS.

outputLengthLimit

outputLengthLimit caps the cumulative length of output written during a render() call, including output from partials rendered via {% render %}.

maxDepth

maxDepth limits how deeply {% render %}, {% include %}, and {% layout %} can nest. Defaults to 128. In sync rendering (renderSync), nested tags are driven by toValueSync, which recursively resumes each yielded generator on the call stack—deep nesting can overflow it, and maxDepth caps that depth. Async render() resumes the same tag generators via toPromise/yield without a deep synchronous call chain, so stack overflow is not a concern there (the limit still applies as a DoS guard).

The memoryLimit option was removed in v11; enforce memory limits at the host or process level instead.

ownPropertyOnly and scope data

With ownPropertyOnly true, plain scope objects only expose own properties (no inherited / Object.prototype keys). Default false follows normal JS property access. Use true for untrusted or polluted objects; add strictVariables if missing paths should error. Override per render via RenderOptions. This is a read policy for scope data—not a sandbox for filters, tags, or your code.

Custom Drop classes

Drop values are not restricted the same way: LiquidJS still reads the prototype chain and may call liquidMethodMissing. You control what a drop exposes; narrow APIs and never feed unsafe data into drops unless the class is built for template access. ownPropertyOnly alone does not harden custom drops—audit them like any privileged code.

Production guidance

LiquidJS does not sandbox template code—custom filters, tags, and scope helpers run as ordinary JavaScript with your process privileges. Built-in DoS limits are one layer; production deployments, especially online services that accept template input, need additional hardening:

  • Prefer curated templates over fully user-defined Liquid when possible; if users need customization, offer a restricted subset rather than open template editing.
  • Run each render in a worker thread or child process with a wall-clock timeout; kill the worker on expiry. Libraries such as paralleljs can help for heavy single-template work.
  • Enforce container/Kubernetes cgroup limits, ulimit, or equivalent on the renderer process for memory and CPU.
  • Apply request rate limits at the API or gateway layer.
  • node:vm, isolated-vm, and Jinja/Twig-style sandbox modes are not a security boundary—template logic runs in the same JS runtime as your app, with your privileges.