Skip to content

fix(xml): harden XsdPreprocessor against XSD-schema XXE / entity-expansion - #261

Draft
ottobolyos wants to merge 1 commit into
TrakHound:masterfrom
ottobolyos:fix/xml-xsd-preprocessor-xxe-hardening
Draft

fix(xml): harden XsdPreprocessor against XSD-schema XXE / entity-expansion#261
ottobolyos wants to merge 1 commit into
TrakHound:masterfrom
ottobolyos:fix/xml-xsd-preprocessor-xxe-hardening

Conversation

@ottobolyos

@ottobolyos ottobolyos commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Hardens MTConnect.NET-XML/XsdPreprocessor.StripXsd11Constructs against XML-entity-expansion (billion-laughs / quadratic-blowup) attacks against operators who point the tooling at an untrusted XSD URL.

Root Cause

Introduced in commit 13c19839 (May 2026) — the original StripXsd11Constructs implementation loaded untrusted XSD text through XDocument.Load(reader, LoadOptions.PreserveWhitespace) with the default XmlReaderSettings. Those defaults leave DoS-shape limits unbounded (MaxCharactersInDocument = 0, MaxCharactersFromEntities = 0 both mean "no limit"), making the preprocessor a viable target for classic XML entity-expansion attacks.

Fix

Route the load through an explicitly-tightened XmlReader:

  • DtdProcessing = Prohibit — the parser refuses any DOCTYPE declaration; entity expansion is impossible.
  • XmlResolver = null — external references are never resolved even if a DTD slipped through some future path.
  • MaxCharactersInDocument = 10_000_000 — bounded parser memory. Sized ~6× larger than the largest current MTConnect XSD schema in the repo (MTConnectStreams_2.7.xsd at 1.7 MB) with generous headroom for schema growth.
  • MaxCharactersFromEntities = 0 — no expansion capacity even in the DTD-permitted path.

Scope

Single file (libraries/MTConnect.NET-XML/XsdPreprocessor.cs) + new dedicated test file. No public API surface change — the hardening is transparent to callers that pass well-formed, bounded XSD input.

Depends on

None. Independent of every other in-flight PR.

@ottobolyos
ottobolyos force-pushed the fix/xml-xsd-preprocessor-xxe-hardening branch from 89252ba to 784a300 Compare August 26, 2026 04:55
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 26, 2026
…integration/up-to-pr-261

Cascade rebuild after upstream/master advanced to 0ddc3c6 (PR TrakHound#220 merge).
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 26, 2026
…#261's new test file

integration-branch-only glue fix, same class as the TrakHound#239/TrakHound#241/TrakHound#249
fixes already on this branch. TrakHound#261 builds clean standalone against
current upstream/master without NUnit 4 present.
@ottobolyos
ottobolyos force-pushed the fix/xml-xsd-preprocessor-xxe-hardening branch from 784a300 to c2d7ea5 Compare August 26, 2026 16:24
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 26, 2026
…integration/up-to-pr-261 on top of integration/up-to-pr-249
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 26, 2026
…integration/up-to-pr-261 on top of previous cascade tip
XsdPreprocessor.StripXsd11Constructs previously loaded untrusted XSD text through
XDocument.Load with the default XmlReaderSettings. Those defaults leave the
DocumentDoS-shape limits unbounded (MaxCharactersInDocument = 0,
MaxCharactersFromEntities = 0 both mean "no limit"), making the preprocessor a viable
target for the classic billion-laughs / quadratic-blowup XML entity-expansion attacks
against an operator who points the tooling at an untrusted XSD URL.

Route the load through an explicitly-tightened XmlReader:
- DtdProcessing = Prohibit — the parser refuses any DOCTYPE declaration; entity
  expansion is impossible.
- XmlResolver = null — external references are never resolved even if a DTD slipped
  through some future path.
- MaxCharactersInDocument = 10,000,000 — bounded parser memory.
- MaxCharactersFromEntities = 0 — no expansion capacity even in the DTD-permitted
  future.

Also add a pre-parse size gate (MaxSourceCharacters = 10 MB) that fails loud with
XmlException before the parser allocates. Silently returning the raw source on an
oversized payload would hand it down to XmlSchema.Read, which lacks a matching cap.

Tests: XsdPreprocessorSecurityTests pins four cases — oversized source raises
XmlException; external DTD reference lands in the parse-error path; internal DTD
entity expansion (billion-laughs shape) lands in the same path; a well-formed small
XSD still round-trips through the hardened loader (happy-path regression guard).
@ottobolyos
ottobolyos force-pushed the fix/xml-xsd-preprocessor-xxe-hardening branch from c2d7ea5 to 99d9cc5 Compare August 27, 2026 02:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants