fix(date): cap strftime widths and account padding in memoryLimit (#895)

* fix(date): cap strftime widths and account padding in memoryLimit

- Clamp numeric strftime pad widths to MAX_STRFTIME_PAD (1024)
- Export estimateStrftimePaddingMemory for the date filter to charge memoryLimit
- Replace unbounded pad() concatenation loop with ch.repeat + single concat
- Add regression tests for clamping and memoryLimit on huge %width directives

Co-authored-by: Cursor <[email protected]>

* fix(date): harden strftime memory accounting and document security model

Move strftime memory charging into the same formatting path used for padding, enforce pre-allocation checks, and add regression tests for non-string date format PoCs. Add dedicated docs clarifying that memoryLimit is cooperative DoS mitigation and not strict heap isolation.

Co-authored-by: Cursor <[email protected]>

* docs(zh-cn): add security model docs for DoS limits

Add a Chinese security-model tutorial and link it from the Chinese DoS guide to clarify that memoryLimit is cooperative accounting, list uncounted custom conversion cases, and recommend avoiding fully user-defined templates in online services.

Co-authored-by: Cursor <[email protected]>

* docs: consolidate DoS docs into security-model pages

Merge DoS guidance into security-model docs in both English and Chinese, and remove the placeholder dos.md pages to avoid duplicate/redirect-only docs.

Co-authored-by: Cursor <[email protected]>

* docs: merge DoS details into security-model docs

Move the detailed parseLimit/renderLimit/memoryLimit explanations and examples into the English and Chinese security-model pages so content from the removed dos pages is preserved.

Co-authored-by: Cursor <[email protected]>

* docs: reorganize security-model structure for clarity

Restructure English and Chinese security-model docs into a consistent flow: security boundary, limits overview, per-limit details, and online service guidance.

Co-authored-by: Cursor <[email protected]>

* refactor(strftime): simplify %N width parsing logic

Use regex-backed width assumptions to simplify %N width normalization and padding memory accounting while keeping behavior equivalent.

Co-authored-by: Cursor <[email protected]>

* 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]>

* fix(strftime): use add() once for padding, minimize churn

- pad(): replace per-char loop with a single add(str, ch.repeat(n)) call.
  The earlier `probe[0] === ch` heuristic was wrong when ch happened to
  equal a leading char of 'probe' (e.g. ch === 'p').
- strftime.ts: revert unrelated typing/structural refactors so the diff
  contains only the memoryLimit threading and the %N memory charge.
- docs: rewire the deleted dos.html sidebar entry to security-model.html
  (with localized labels) so the deleted page does not 404 from the
  sidebar.

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Yang Jun
2026-05-10 14:35:28 +08:00
committed by GitHub
co-authored by Cursor
parent 5b9c346908
commit 3129d46dc9
11 changed files with 200 additions and 127 deletions
+8 -4
View File
@@ -1,11 +1,13 @@
import { changeCase, padStart, padEnd } from './underscore'
import { LiquidDate } from './liquid-date'
import type { Limiter } from './limiter'
const rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/
interface FormatOptions {
flags: object;
width?: string;
modifier?: string;
memoryLimit?: Pick<Limiter, 'use'>;
}
// prototype extensions
@@ -95,6 +97,7 @@ const formatCodes = {
N: (d: LiquidDate, opts: FormatOptions) => {
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')
},
p: (d: LiquidDate) => (d.getHours() < 12 ? 'AM' : 'PM'),
@@ -118,25 +121,25 @@ const formatCodes = {
};
(formatCodes as any).h = formatCodes.b
export function strftime (d: LiquidDate, formatStr: string) {
export function strftime (d: LiquidDate, formatStr: string, memoryLimit?: Pick<Limiter, 'use'>) {
let output = ''
let remaining = formatStr
let match
while ((match = rFormat.exec(remaining))) {
output += remaining.slice(0, match.index)
remaining = remaining.slice(match.index + match[0].length)
output += format(d, match)
output += format(d, match, memoryLimit)
}
return output + remaining
}
function format (d: LiquidDate, match: RegExpExecArray) {
function format (d: LiquidDate, match: RegExpExecArray, memoryLimit?: Pick<Limiter, 'use'>) {
const [input, flagStr = '', width, modifier, conversion] = match
const convert = formatCodes[conversion]
if (!convert) return input
const flags = {}
for (const flag of flagStr) flags[flag] = true
let ret = String(convert(d, { flags, width, modifier }))
let ret = String(convert(d, { flags, width, modifier, memoryLimit }))
let padChar = padSpaceChars.has(conversion) ? ' ' : '0'
let padWidth = width || padWidths[conversion] || 0
if (flags['^']) ret = ret.toUpperCase()
@@ -144,5 +147,6 @@ function format (d: LiquidDate, match: RegExpExecArray) {
if (flags['_']) padChar = ' '
else if (flags['0']) padChar = '0'
if (flags['-']) padWidth = 0
memoryLimit?.use(Number(padWidth) - ret.length)
return padStart(ret, padWidth, padChar)
}
+3 -3
View File
@@ -152,9 +152,9 @@ export function padEnd (str: any, length: number, ch = ' ') {
export function pad (str: any, length: number, ch: string, add: (str: string, ch: string) => string) {
str = String(str)
let n = length - str.length
while (n-- > 0) str = add(str, ch)
return str
const n = length - str.length
if (n <= 0) return str
return add(str, ch.repeat(n))
}
export function identify<T> (val: T): T {