From b405733cdbaea013af9dc4c6ca8d379ebbeccb64 Mon Sep 17 00:00:00 2001 From: Yang Jun Date: Sun, 10 May 2026 13:34:37 +0800 Subject: [PATCH] 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 --- docs/source/tutorials/security-model.md | 2 +- docs/source/zh-cn/tutorials/security-model.md | 2 +- src/util/strftime.spec.ts | 9 --------- src/util/strftime.ts | 6 +----- test/integration/filters/date.spec.ts | 9 ++++++--- 5 files changed, 9 insertions(+), 19 deletions(-) diff --git a/docs/source/tutorials/security-model.md b/docs/source/tutorials/security-model.md index 210d7b688..f4021639b 100644 --- a/docs/source/tutorials/security-model.md +++ b/docs/source/tutorials/security-model.md @@ -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. -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 diff --git a/docs/source/zh-cn/tutorials/security-model.md b/docs/source/zh-cn/tutorials/security-model.md index b21e9f196..ce3cdd17c 100644 --- a/docs/source/zh-cn/tutorials/security-model.md +++ b/docs/source/zh-cn/tutorials/security-model.md @@ -38,7 +38,7 @@ LiquidJS 提供了面向 DoS 的限制选项(`parseLimit`、`renderLimit`、`m 渲染时间是在渲染每个模板之前检查的。在上面的例子中,循环中有两个模板:`order: ` 和 `{{i}}`,因此会检查 2x10000000 次。 -单个模板内的标签和过滤器仍然可能把进程挂起。 +`renderLimit` 不是硬性的 CPU 限制器。它是在模板渲染边界做检查,因此检查点之间的高计算开销过滤器/标签/用户自定义函数,或深层模板嵌套,仍可能导致 DoS。 ### memoryLimit diff --git a/src/util/strftime.spec.ts b/src/util/strftime.spec.ts index 60cc0e3d1..b629b6d98 100644 --- a/src/util/strftime.spec.ts +++ b/src/util/strftime.spec.ts @@ -31,10 +31,6 @@ describe('util/strftime', function () { 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 () { const first = new TestDate('2016-03-01 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, '%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 () { expect(t(now, '%p')).toBe('PM') expect(t(then, '%p')).toBe('AM') diff --git a/src/util/strftime.ts b/src/util/strftime.ts index f1fe0dcfb..971de58b8 100644 --- a/src/util/strftime.ts +++ b/src/util/strftime.ts @@ -2,9 +2,6 @@ import { changeCase, padStart, padEnd } from './underscore' import { LiquidDate } from './liquid-date' 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])?(.)/ interface FormatOptions { flags: Record; @@ -98,7 +95,7 @@ const formatCodes: Record unknow m: (d: LiquidDate) => d.getMonth() + 1, M: (d: LiquidDate) => d.getMinutes(), 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) opts.memoryLimit?.use(width - str.length) return padEnd(str, width, '0') @@ -151,7 +148,6 @@ function format (d: LiquidDate, match: RegExpExecArray, memoryLimit?: Pick MAX_STRFTIME_PAD) padWidth = MAX_STRFTIME_PAD memoryLimit?.use(Math.max(0, padWidth - ret.length)) return padStart(ret, padWidth, padChar) diff --git a/test/integration/filters/date.spec.ts b/test/integration/filters/date.spec.ts index de523c8e0..992876906 100644 --- a/test/integration/filters/date.spec.ts +++ b/test/integration/filters/date.spec.ts @@ -222,10 +222,13 @@ describe('filters/date', function () { expect(() => liquid.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f })) .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 out = liquid.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f: '%50000d' }) - expect(out.length).toBe(1024) + const out = liquid.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f: '%5000d' }) + 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') }) }) })