Skip to content

Commit e443068

Browse files
committed
feat: DoS prevention, #250
1 parent fad00aa commit e443068

31 files changed

+469
-133
lines changed

docs/source/_data/sidebar.yml

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ tutorials:
1919
plugins: plugins.html
2020
operators: operators.html
2121
truth: truthy-and-falsy.html
22+
dos: dos.html
2223
miscellaneous:
2324
migration9: migrate-to-9.html
2425
changelog: changelog.html

docs/source/tutorials/dos.md

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
title: DoS Prevention
3+
---
4+
5+
When the template or data context cannot be trusted, enabling DoS prevention options is crucial. LiquidJS provides 3 options for this purpose: `parseLimit`, `renderLimit`, and `memoryLimit`.
6+
7+
## TL;DR
8+
9+
Setting these options can largely ensure that your LiquidJS instance won't hang for extended periods or consume excessive memory. These limits are based on the available JavaScript APIs, so they are not precise hard limits but thresholds to help prevent your process from failing or hanging.
10+
11+
```typescript
12+
const liquid = new Liquid({
13+
parseLimit: 1e8, // typical size of your templates in each render
14+
renderLimit: 1000, // limit each render to be completed in 1s
15+
memoryLimit: 1e9, // memory available for LiquidJS (1e9 for 1GB)
16+
})
17+
```
18+
19+
When a `parse()` or `render()` cannot be completed within given resource, it throws.
20+
21+
## parseLimit
22+
23+
[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.
24+
25+
A typical PC handles `1e8` (100M) characters without issues.
26+
27+
## renderLimit
28+
29+
Restricting template size alone is insufficient because dynamic loops with large counts can occur in render time. [renderLimit][renderLimit] mitigates this by limiting the time consumed by each `render()` call.
30+
31+
```liquid
32+
{%- for i in (1..10000000) -%}
33+
order: {{i}}
34+
{%- endfor -%}
35+
```
36+
37+
Render time is checked on a per-template basis (before rendering each template). In the above example, there are 2 templates in the loop: `order: ` and `{{i}}`, render time will be checked 10000000x2 times.
38+
39+
For time-consuming tags and filters within a single template, the process can still hang. For fully controlled rendering, consider using a process manager like [paralleljs][paralleljs].
40+
41+
## memoryLimit
42+
43+
Even with small number of templates and iterations, memory usage can grow exponentially. In the following example, memory doubles with each iteration:
44+
45+
```liquid
46+
{% assign array = "1,2,3" | split: "," %}
47+
{% for i in (1..32) %}
48+
{% assign array = array | concat: array %}
49+
{% endfor %}
50+
```
51+
52+
[memoryLimit][memoryLimit] restricts memory-sensitive filters to prevent excessive memory allocation. As [JavaScript uses GC to manage memory](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management), `memoryLimit` limits only the total number of objects allocated by memory sensitive filters in LiquidJS thus may not reflect the actual memory footprint.
53+
54+
[paralleljs]: https://www.npmjs.com/package/paralleljs
55+
[parseLimit]: /api/interfaces/LiquidOptions.html#parseLimit
56+
[renderLimit]: /api/interfaces/LiquidOptions.html#renderLimit
57+
[memoryLimit]: /api/interfaces/LiquidOptions.html#memoryLimit

docs/source/zh-cn/tutorials/dos.md

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
title: 防止 DoS 攻击
3+
---
4+
5+
当模板或数据上下文不可信时,启用DoS预防选项至关重要。LiquidJS 提供了三个选项用于此目的:`parseLimit``renderLimit``memoryLimit`
6+
7+
## TL;DR
8+
9+
设置这些选项可以在很大程度上确保你的 LiquidJS 实例不会长时间挂起或消耗过多内存。这些限制基于可用的 JavaScript API,因此它们不是精确的硬性限制,而是确保你的进程不会失败或挂起的阈值。
10+
11+
```typescript
12+
const liquid = new Liquid({
13+
parseLimit: 1e8, // 每次渲染的模板的典型大小
14+
renderLimit: 1000, // 每次渲染最多 1s
15+
memoryLimit: 1e9, // LiquidJS 可用的内存(1e9 对应 1GB)
16+
})
17+
```
18+
19+
## parseLimit
20+
21+
[parseLimit][parseLimit] 限制每次 `.parse()` 调用中解析的模板大小(字符长度),包括引用的 partials 和 layouts。由于 LiquidJS 解析模板字符串的时间复杂度接近 O(n),限制模板总长度通常就足够了。
22+
23+
普通电脑可以很容易处理 `1e8`(100M)个字符的模板。
24+
25+
## renderLimit
26+
27+
仅限制模板大小是不够的,因为在渲染时可能会出现动态的数组和循环。[renderLimit][renderLimit] 通过限制每次 `render()` 调用的时间来缓解这些问题。
28+
29+
```liquid
30+
{%- for i in (1..10000000) -%}
31+
order: {{i}}
32+
{%- endfor -%}
33+
```
34+
35+
渲染时间是在渲染每个模板之前检查的。在上面的例子中,循环中有两个模板:`order: ``{{i}}`,因此会检查 2x10000000 次。
36+
37+
单个模板内的标签和过滤器仍然可能把进程挂起。要完全控制渲染过程,建议使用类似 [paralleljs][paralleljs] 的进程管理器。
38+
39+
## memoryLimit
40+
41+
即使模板和迭代次数较少,内存使用量也可能呈指数增长。在下面的示例中,内存会在每次迭代中翻倍:
42+
43+
```liquid
44+
{% assign array = "1,2,3" | split: "," %}
45+
{% for i in (1..32) %}
46+
{% assign array = array | concat: array %}
47+
{% endfor %}
48+
```
49+
50+
[memoryLimit][memoryLimit] 限制内存敏感的过滤器,以防止过度的内存分配。由于 [JavaScript 使用 GC 来管理内存](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management)`memoryLimit` 仅限制 LiquidJS 中内存敏感过滤器分配的对象总数,因此可能无法反映实际的内存占用。
51+
52+
[paralleljs]: https://www.npmjs.com/package/paralleljs
53+
[parseLimit]: /api/interfaces/LiquidOptions.html#parseLimit
54+
[renderLimit]: /api/interfaces/LiquidOptions.html#renderLimit
55+
[memoryLimit]: /api/interfaces/LiquidOptions.html#memoryLimit

docs/themes/navy/languages/en.yml

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ sidebar:
5151
plugins: Plugins
5252
operators: Operators
5353
truth: Truthy and Falsy
54+
dos: DoS
5455

5556
miscellaneous: Miscellaneous
5657
migration9: 'Migrate to LiquidJS 9'

docs/themes/navy/languages/zh-cn.yml

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ sidebar:
5151
plugins: 插件
5252
operators: 运算符
5353
truth: 真和假
54+
dos: DoS
5455

5556
miscellaneous: 其他
5657
migration9: '迁移到 LiquidJS 9'

src/context/context.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Drop } from '../drop/drop'
22
import { __assign } from 'tslib'
33
import { NormalizedFullOptions, defaultOptions, RenderOptions } from '../liquid-options'
44
import { Scope } from './scope'
5-
import { isArray, isNil, isUndefined, isString, isFunction, toLiquid, InternalUndefinedVariableError, toValueSync, isObject } from '../util'
5+
import { isArray, isNil, isUndefined, isString, isFunction, toLiquid, InternalUndefinedVariableError, toValueSync, isObject, Limiter } from '../util'
66

77
type PropertyKey = string | number;
88

@@ -33,13 +33,17 @@ export class Context {
3333
*/
3434
public strictVariables: boolean;
3535
public ownPropertyOnly: boolean;
36-
public constructor (env: object = {}, opts: NormalizedFullOptions = defaultOptions, renderOptions: RenderOptions = {}) {
36+
public memoryLimit: Limiter;
37+
public renderLimit: Limiter;
38+
public constructor (env: object = {}, opts: NormalizedFullOptions = defaultOptions, renderOptions: RenderOptions = {}, { memoryLimit, renderLimit }: { [key: string]: Limiter } = {}) {
3739
this.sync = !!renderOptions.sync
3840
this.opts = opts
3941
this.globals = renderOptions.globals ?? opts.globals
4042
this.environments = isObject(env) ? env : Object(env)
4143
this.strictVariables = renderOptions.strictVariables ?? this.opts.strictVariables
4244
this.ownPropertyOnly = renderOptions.ownPropertyOnly ?? opts.ownPropertyOnly
45+
this.memoryLimit = memoryLimit ?? new Limiter('memory alloc', renderOptions.memoryLimit ?? opts.memoryLimit)
46+
this.renderLimit = renderLimit ?? new Limiter('template render', performance.now() + (renderOptions.templateLimit ?? opts.renderLimit))
4347
}
4448
public getRegister (key: string) {
4549
return (this.registers[key] = this.registers[key] || {})
@@ -95,6 +99,16 @@ export class Context {
9599
public bottom () {
96100
return this.scopes[0]
97101
}
102+
public spawn (scope = {}) {
103+
return new Context(scope, this.opts, {
104+
sync: this.sync,
105+
globals: this.globals,
106+
strictVariables: this.strictVariables
107+
}, {
108+
renderLimit: this.renderLimit,
109+
memoryLimit: this.memoryLimit
110+
})
111+
}
98112
private findScope (key: string | number) {
99113
for (let i = this.scopes.length - 1; i >= 0; i--) {
100114
const candidate = this.scopes[i]
@@ -108,7 +122,7 @@ export class Context {
108122
export function readProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean) {
109123
obj = toLiquid(obj)
110124
if (isNil(obj)) return obj
111-
if (isArray(obj) && key < 0) return obj[obj.length + +key]
125+
if (isArray(obj) && (key as number) < 0) return obj[obj.length + +key]
112126
const value = readJSProperty(obj, key, ownPropertyOnly)
113127
if (value === undefined && obj instanceof Drop) return obj.liquidMethodMissing(key)
114128
if (isFunction(value)) return value.call(obj)

0 commit comments

Comments
 (0)