BTC
ETH
SOL
BNB
GOLD
XRP
DOGE
ADA
Back to home
Security

[HIGH] Security Advisory: Pretext: Algorithmic Complexity (DoS) in the text analysis phase (@chenglou/pretext)

A vulnerability in the @chenglou/pretext library exposes applications to denial-of-service attacks through algorithmic complexity.

A vulnerability in the @chenglou/pretext library exposes applications to denial-of-service attacks through algorithmic complexity. The function isRepeatedSingleCharRun() in src/analysis.ts (line 285) rescans entire text segments repeatedly during merging, leading to O(n²) time complexity on inputs of repeated identical punctuation characters. Attackers controlling input to the prepare() function can block the main thread for about 20 seconds using just 80KB of data, such as "(".repeat(80_000). This hit was tested on commit 9364741d3562fcc65aacc50953e867a5cb9fdb23 (v0.0.4) with Node.js v24.12.0 on Windows x64.

Pretext is a JavaScript text layout engine designed for precise measurement and shaping, relying on browser APIs like Intl.Segmenter. Developers use it in React Native, web apps, or Node.js services for tasks like dynamic text wrapping or canvas rendering. If your app passes untrusted text—user comments, chat messages, or imported documents—straight to prepare(), this turns a small payload into a full hang. In browsers, it freezes tabs; in servers, it spikes CPU and delays requests.

Root Cause

The issue sits in buildMergedSegmentation() (line 795), which processes segments from Intl.Segmenter with granularity: 'word'. This API splits punctuation into individual segments. For N identical characters like parentheses, the code attempts to merge them into one segment. Before each append, it calls isRepeatedSingleCharRun(accumulated_segment, new_char) to confirm uniformity.

That check loops over the full accumulated string every time:

// analysis.ts:285-291
function isRepeatedSingleCharRun(segment: string, ch: string): boolean {
  if (segment.length === 0) return false
  for (const part of segment) {  // <- Iterates ENTIRE accumulated string
    if (part !== ch) return false
  }
  return true
}

On the k-th merge (k from 1 to N), it scans k characters. Total comparisons: sum_{k=1 to N} k = N(N+1)/2, pure quadratic. Triggers only for single chars classified as ‘text’ by classifySegmentBreakChar (line 321)—no spaces, hyphens, or whitespace. Common culprits: (, ), [, ], !, #, $, etc.

Call chain: prepare(text, font)analyzeText()buildMergedSegmentation() → merge loop → isRepeatedSingleCharRun(). No bounds checks or optimizations mitigate this.

Proof of Concept

Reproduce with this Node.js script:

import { prepare } from '@chenglou/pretext'
// 80,000 characters -> ~20 seconds of main-thread blocking
const payload = '('.repeat(80_000)
console.time('pretext-prepare')
prepare(payload, '16px Arial')
console.timeEnd('pretext-prepare')

Time it yourself—expect 15-25 seconds depending on hardware. Scales with input size: 50k chars might take 8 seconds; 100k pushes 30+. Works in browsers too; paste into a dev tool console using the lib.

Why This Matters and Fixes

Quadratic scans like this are a classic trap, especially with segmentation APIs that tokenize aggressively. Pretext aims for subpixel accuracy in text layout, but this flaw undermines reliability in untrusted environments. Web apps rendering markdown or user profiles risk tab crashes from pasted text. Node microservices measuring email subjects could DDoS themselves. At 80KB, payloads fit in URLs or JSON bodies, evading naive length limits.

Check your deps: v0.0.4 is current as of this advisory, but pretext is young—1.3k weekly downloads on npm, used in niche layout tools. If exposed, input validation helps: strip repeated chars or cap lengths at 10k. Better: patch the lib. Fix is trivial—track run length and char type in state, skip the scan. Replace the loop with segment === ch.repeat(segment.length) (still O(n) worst-case but amortized fine) or maintain counters during merges.

Reported severity: high, as it weaponizes core text processing. No RCE or data leak, but DoS counts in single-threaded JS. Fair assessment—this isn’t novel, but shows why auditing layout libs matters. Scan your code for prepare(); if user text flows in, act now. Upstream fix pending; watch the repo.

April 9, 2026 · 3 min · 13 views · Source: GitHub Security

Related