mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
refactor(strftime): rely on memoryLimit for width control
Remove MAX_STRFTIME_PAD hard capping and rely on memoryLimit enforcement before padding allocation. Update strftime/date tests and security-model docs to match the new boundary and renderLimit caveats. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -38,7 +38,7 @@ Restricting template size alone is insufficient because dynamic loops with large
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
For time-consuming tags and filters within a single template, the process can still hang.
|
`renderLimit` is not a hard CPU limiter. It is checked between template renders, so compute-intensive filters/tags/user-defined functions or deeply nested template execution between checks can still cause DoS.
|
||||||
|
|
||||||
### memoryLimit
|
### memoryLimit
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ LiquidJS 提供了面向 DoS 的限制选项(`parseLimit`、`renderLimit`、`m
|
|||||||
|
|
||||||
渲染时间是在渲染每个模板之前检查的。在上面的例子中,循环中有两个模板:`order: ` 和 `{{i}}`,因此会检查 2x10000000 次。
|
渲染时间是在渲染每个模板之前检查的。在上面的例子中,循环中有两个模板:`order: ` 和 `{{i}}`,因此会检查 2x10000000 次。
|
||||||
|
|
||||||
单个模板内的标签和过滤器仍然可能把进程挂起。
|
`renderLimit` 不是硬性的 CPU 限制器。它是在模板渲染边界做检查,因此检查点之间的高计算开销过滤器/标签/用户自定义函数,或深层模板嵌套,仍可能导致 DoS。
|
||||||
|
|
||||||
### memoryLimit
|
### memoryLimit
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ describe('util/strftime', function () {
|
|||||||
expect(t(date, '%j')).toBe('061')
|
expect(t(date, '%j')).toBe('061')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('should cap excessive numeric pad width for day-of-month', function () {
|
|
||||||
expect(t(now, '%5d')).toBe('00004')
|
|
||||||
expect(t(now, '%2000d').length).toBe(1024)
|
|
||||||
})
|
|
||||||
it('should format %q as date suffix', function () {
|
it('should format %q as date suffix', function () {
|
||||||
const first = new TestDate('2016-03-01 03:05:03')
|
const first = new TestDate('2016-03-01 03:05:03')
|
||||||
const second = new TestDate('2016-03-02 03:05:03')
|
const second = new TestDate('2016-03-02 03:05:03')
|
||||||
@@ -91,11 +87,6 @@ describe('util/strftime', function () {
|
|||||||
expect(t(time, '%10N')).toBe('1290000000')
|
expect(t(time, '%10N')).toBe('1290000000')
|
||||||
expect(t(time, '%0N')).toBe('129000000')
|
expect(t(time, '%0N')).toBe('129000000')
|
||||||
})
|
})
|
||||||
it('should cap excessive numeric pad width for %N', function () {
|
|
||||||
const time = new TestDate('2019-12-15 01:21:00.129')
|
|
||||||
expect(t(time, '%2000N').length).toBe(1024)
|
|
||||||
expect(t(time, '%50000N').length).toBe(1024)
|
|
||||||
})
|
|
||||||
it('should format %p as upper cased am/pm', function () {
|
it('should format %p as upper cased am/pm', function () {
|
||||||
expect(t(now, '%p')).toBe('PM')
|
expect(t(now, '%p')).toBe('PM')
|
||||||
expect(t(then, '%p')).toBe('AM')
|
expect(t(then, '%p')).toBe('AM')
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import { changeCase, padStart, padEnd } from './underscore'
|
|||||||
import { LiquidDate } from './liquid-date'
|
import { LiquidDate } from './liquid-date'
|
||||||
import type { Limiter } from './limiter'
|
import type { Limiter } from './limiter'
|
||||||
|
|
||||||
/** Upper bound for numeric strftime widths (%N, %15d, …) — avoids unbounded pad / CPU / memory. */
|
|
||||||
export const MAX_STRFTIME_PAD = 1024
|
|
||||||
|
|
||||||
const rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/
|
const rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/
|
||||||
interface FormatOptions {
|
interface FormatOptions {
|
||||||
flags: Record<string, boolean>;
|
flags: Record<string, boolean>;
|
||||||
@@ -98,7 +95,7 @@ const formatCodes: Record<string, (d: LiquidDate, opts: FormatOptions) => unknow
|
|||||||
m: (d: LiquidDate) => d.getMonth() + 1,
|
m: (d: LiquidDate) => d.getMonth() + 1,
|
||||||
M: (d: LiquidDate) => d.getMinutes(),
|
M: (d: LiquidDate) => d.getMinutes(),
|
||||||
N: (d: LiquidDate, opts: FormatOptions) => {
|
N: (d: LiquidDate, opts: FormatOptions) => {
|
||||||
const width = Math.min(Number(opts.width) || 9, MAX_STRFTIME_PAD)
|
const width = Number(opts.width) || 9
|
||||||
const str = String(d.getMilliseconds()).slice(0, width)
|
const str = String(d.getMilliseconds()).slice(0, width)
|
||||||
opts.memoryLimit?.use(width - str.length)
|
opts.memoryLimit?.use(width - str.length)
|
||||||
return padEnd(str, width, '0')
|
return padEnd(str, width, '0')
|
||||||
@@ -151,7 +148,6 @@ function format (d: LiquidDate, match: RegExpExecArray, memoryLimit?: Pick<Limit
|
|||||||
if (flags['_']) padChar = ' '
|
if (flags['_']) padChar = ' '
|
||||||
else if (flags['0']) padChar = '0'
|
else if (flags['0']) padChar = '0'
|
||||||
if (flags['-']) padWidth = 0
|
if (flags['-']) padWidth = 0
|
||||||
else if (padWidth > MAX_STRFTIME_PAD) padWidth = MAX_STRFTIME_PAD
|
|
||||||
|
|
||||||
memoryLimit?.use(Math.max(0, padWidth - ret.length))
|
memoryLimit?.use(Math.max(0, padWidth - ret.length))
|
||||||
return padStart(ret, padWidth, padChar)
|
return padStart(ret, padWidth, padChar)
|
||||||
|
|||||||
@@ -222,10 +222,13 @@ describe('filters/date', function () {
|
|||||||
expect(() => liquid.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f }))
|
expect(() => liquid.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f }))
|
||||||
.toThrow('memory alloc limit exceeded')
|
.toThrow('memory alloc limit exceeded')
|
||||||
})
|
})
|
||||||
it('should clamp numeric strftime pad width', () => {
|
it('should honor numeric strftime pad width when memoryLimit allows', () => {
|
||||||
const liquid = new Liquid({ memoryLimit: 1e7 })
|
const liquid = new Liquid({ memoryLimit: 1e7 })
|
||||||
const out = liquid.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f: '%50000d' })
|
const out = liquid.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f: '%5000d' })
|
||||||
expect(out.length).toBe(1024)
|
expect(out.length).toBe(5000)
|
||||||
|
const tight = new Liquid({ memoryLimit: 100 })
|
||||||
|
expect(() => tight.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f: '%5000d' }))
|
||||||
|
.toThrow('memory alloc limit exceeded')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user