can1357/oh-my-pi · error

Entity expansion limit exceeded

Error message

Entity expansion limit exceeded

What it means

XmlParser.#expandEntities() expands custom DTD entities recursively while tracking total expansions. Each expansion increments a counter, and once it exceeds `maxTotalExpansions` the parser throws "Entity expansion limit exceeded". This is a billion-laughs / XML-bomb protection: it caps the CPU and memory cost of recursive entity expansion across the whole document.

Source

Thrown at packages/utils/src/xml.ts:264

		const entityPattern = /<!ENTITY\s+([^\s]+)\s+(["'])(.*?)\2\s*>/gs;
		for (const match of declaration.matchAll(entityPattern)) this.#entities.set(match[1]!, match[3]!);
	}

	#normalizeText(value: string, entities: boolean): string {
		let normalized = this.#settings.trimValues ? value.trim() : value;
		if (entities && this.#settings.processEntities && normalized.includes("&"))
			normalized = this.#expandEntities(normalized);
		return normalized;
	}

	#expandEntities(value: string, stack?: ReadonlySet<string>): string {
		return value.replace(/&([A-Za-z_:][\w.:-]*);/g, (whole, name: string) => {
			const standard = STANDARD_ENTITIES[name];
			if (standard !== undefined) return standard;
			const replacement = this.#entities.get(name);
			if (replacement === undefined || stack?.has(name)) return whole;
			this.#expansions++;
			if (this.#expansions > this.#settings.maxTotalExpansions) throw new Error("Entity expansion limit exceeded");
			const nextStack = new Set(stack);
			nextStack.add(name);
			return this.#expandEntities(replacement, nextStack);
		});
	}

	#addValue(
		target: XmlObject,
		name: string,
		path: string,
		value: unknown,
		leaf: boolean,
		attribute: boolean | null,
	): void {
		const present = Object.hasOwn(target, name);
		if (present) {
			const current = target[name];
			if (Array.isArray(current)) current.push(value);

View on GitHub (pinned to 9690622007)

Solutions

  1. Reject the document if untrusted — the limit exists precisely for hostile input; treat this error as a security signal.
  2. If the input is trusted, raise `maxTotalExpansions` in the parser settings to accommodate legitimate entity use.
  3. Pre-expand or remove the DTD entity definitions upstream, or set processEntities: false if entity substitution is unnecessary.

Example fix

// before
new XmlParser(); // defaults cap total expansions
// after
new XmlParser({ maxTotalExpansions: 10_000 }); // trusted large config only
Defensive patterns

Strategy: try-catch

Validate before calling

function docUsesDtdEntities(xml) {
  return /<!DOCTYPE[^>[]*(\[|<!ENTITY)/s.test(xml) || /&[A-Za-z_:][\w.:-]*;/.test(xml);
}
// For untrusted input, reject documents with custom DTD entities before parsing:
if (!trusted && /<!ENTITY/.test(xml)) throw new Error('Untrusted XML with DTD entities rejected');

Try / catch

try {
  return parser.parse(xml);
} catch (err) {
  if (err instanceof Error && err.message === 'Entity expansion limit exceeded') {
    // treat as suspected XML bomb: reject the document / quarantine the sender
    throw new Error('XML rejected: entity expansion limit exceeded (possible XML bomb)');
  }
  throw err;
}

Prevention

When it happens

Trigger: Parsing XML whose DOCTYPE defines entities that expand to large or nested-expanding replacements (classic `&laugh;`/`&lol9;` bombs), so total expansions surpass settings.maxTotalExpansions; also many distinct entity uses across a large document under a low configured limit.

Common situations: Processing untrusted third-party XML (feeds, SAML-ish payloads, office documents) crafted to blow up entity expansion; legitimately huge config files with thousands of entity references after lowering maxTotalExpansions for security.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/750a1931aac0a614. Report an issue: GitHub.