{"record":{"id":"750a1931aac0a614","repo":"can1357/oh-my-pi","slug":"entity-expansion-limit-exceeded","errorCode":null,"errorMessage":"Entity expansion limit exceeded","messagePattern":"Entity expansion limit exceeded","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/utils/src/xml.ts","lineNumber":264,"sourceCode":"\t\tconst entityPattern = /<!ENTITY\\s+([^\\s]+)\\s+([\"'])(.*?)\\2\\s*>/gs;\n\t\tfor (const match of declaration.matchAll(entityPattern)) this.#entities.set(match[1]!, match[3]!);\n\t}\n\n\t#normalizeText(value: string, entities: boolean): string {\n\t\tlet normalized = this.#settings.trimValues ? value.trim() : value;\n\t\tif (entities && this.#settings.processEntities && normalized.includes(\"&\"))\n\t\t\tnormalized = this.#expandEntities(normalized);\n\t\treturn normalized;\n\t}\n\n\t#expandEntities(value: string, stack?: ReadonlySet<string>): string {\n\t\treturn value.replace(/&([A-Za-z_:][\\w.:-]*);/g, (whole, name: string) => {\n\t\t\tconst standard = STANDARD_ENTITIES[name];\n\t\t\tif (standard !== undefined) return standard;\n\t\t\tconst replacement = this.#entities.get(name);\n\t\t\tif (replacement === undefined || stack?.has(name)) return whole;\n\t\t\tthis.#expansions++;\n\t\t\tif (this.#expansions > this.#settings.maxTotalExpansions) throw new Error(\"Entity expansion limit exceeded\");\n\t\t\tconst nextStack = new Set(stack);\n\t\t\tnextStack.add(name);\n\t\t\treturn this.#expandEntities(replacement, nextStack);\n\t\t});\n\t}\n\n\t#addValue(\n\t\ttarget: XmlObject,\n\t\tname: string,\n\t\tpath: string,\n\t\tvalue: unknown,\n\t\tleaf: boolean,\n\t\tattribute: boolean | null,\n\t): void {\n\t\tconst present = Object.hasOwn(target, name);\n\t\tif (present) {\n\t\t\tconst current = target[name];\n\t\t\tif (Array.isArray(current)) current.push(value);","sourceCodeStart":246,"sourceCodeEnd":282,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/xml.ts#L246-L282","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nnew XmlParser(); // defaults cap total expansions\n// after\nnew XmlParser({ maxTotalExpansions: 10_000 }); // trusted large config only","handlingStrategy":"try-catch","validationCode":"function docUsesDtdEntities(xml) {\n  return /<!DOCTYPE[^>[]*(\\[|<!ENTITY)/s.test(xml) || /&[A-Za-z_:][\\w.:-]*;/.test(xml);\n}\n// For untrusted input, reject documents with custom DTD entities before parsing:\nif (!trusted && /<!ENTITY/.test(xml)) throw new Error('Untrusted XML with DTD entities rejected');","typeGuard":null,"tryCatchPattern":"try {\n  return parser.parse(xml);\n} catch (err) {\n  if (err instanceof Error && err.message === 'Entity expansion limit exceeded') {\n    // treat as suspected XML bomb: reject the document / quarantine the sender\n    throw new Error('XML rejected: entity expansion limit exceeded (possible XML bomb)');\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["xml","security","entity-expansion","billion-laughs"],"backgroundTag":"xml-entity-expansion-limit","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}