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
- Reject the document if untrusted — the limit exists precisely for hostile input; treat this error as a security signal.
- If the input is trusted, raise `maxTotalExpansions` in the parser settings to accommodate legitimate entity use.
- 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
- For untrusted XML, strip or reject DOCTYPE/entity declarations before parsing.
- Keep maxTotalExpansions low by default; raise it only for vetted internal documents.
- Alert on this error — repeated occurrences usually indicate hostile input.
- Pre-expand known-good entity sets at authoring time instead of at parse time.
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
- ${source} response is missing credentials.
- The managed-skills root is a symlink; refusing to operate ou
- Managed skill "${name}" SKILL.md is a symlink; refusing to o
- Managed skill "${name}" resolves through a symlink; refusing
- Managed skill "${safe}" is a symlink; refusing to delete out
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/750a1931aac0a614.
Report an issue: GitHub.