PureXML/Sources/Parsing/Limits.swift
Mihaela Mihaljevic 01a35cc6e6 feat(parser): optional DTD with internal entities and billion-laughs defense
Add opt-in DTD processing behind Limits(allowDoctype:), off by default. When on,
DoctypeScanner reads the <!DOCTYPE> internal subset and honors internal
<!ENTITY name "value"> general entities, which EntityDecoder expands recursively
in text and attribute values.

Safety is mandatory and reimplemented from the Expat amplification-accounting
model (no upstream code copied):
- only entity-expanded characters count against a shared budget
  (Limits.maxEntityExpansion); overflow raises amplificationLimitExceeded, which
  defeats billion-laughs.
- a visiting set detects recursive entities (recursiveEntity).
- undeclared references raise undefinedEntity.
- external entities are never stored or loaded, so XXE stays closed.

The default still rejects <!DOCTYPE>. 8 DTD tests (incl. billion-laughs and the
external-entity refusal); 49 total.
2026-06-08 01:17:09 +02:00

44 lines
2.0 KiB
Swift

public extension PureXML.Parsing {
/// Configurable, bounded-by-default parser limits. Exceeding any of them is a
/// parse error rather than unbounded work, which protects against pathological
/// input (deeply nested or oversized documents). The depth cap also keeps the
/// recursive ``PureXML/Model/Node`` value safe to build and tear down. The
/// defaults follow libxml2's posture.
struct Limits: Equatable, Sendable {
/// Maximum element nesting depth (libxml2 defaults to 256).
public var maxDepth: Int
/// Maximum length of a single name (element, attribute, or PI target).
public var maxNameLength: Int
/// Maximum length of a single text, CDATA, comment, or PI-data run.
public var maxContentLength: Int
/// Whether to process a `<!DOCTYPE>` internal subset at all. Off by default:
/// rejecting DTDs removes the XXE and entity-expansion threat classes. When
/// on, only internal general entities are honored; external entities are
/// still refused, and expansion is bounded by ``maxEntityExpansion``.
public var allowDoctype: Bool
/// The maximum number of characters that internal-entity expansion may
/// produce across the whole document. The cap (not the literal document
/// size) is what defends against billion-laughs amplification.
public var maxEntityExpansion: Int
public init(
maxDepth: Int = 256,
maxNameLength: Int = 50000,
maxContentLength: Int = 10_000_000,
allowDoctype: Bool = false,
maxEntityExpansion: Int = 1_000_000,
) {
self.maxDepth = maxDepth
self.maxNameLength = maxNameLength
self.maxContentLength = maxContentLength
self.allowDoctype = allowDoctype
self.maxEntityExpansion = maxEntityExpansion
}
/// The default limits, matching libxml2's bounded-by-default posture.
public static let `default` = Limits()
}
}