Skip to content

fix(webdav): reject document type declarations in WebDAV XML request bodies - #37167

Queued
swicken wants to merge 7 commits into
mainfrom
issue-671-webdav-xml-request-validation
Queued

fix(webdav): reject document type declarations in WebDAV XML request bodies#37167
swicken wants to merge 7 commits into
mainfrom
issue-671-webdav-xml-request-validation

Conversation

@swicken

@swicken swicken commented Aug 24, 2026

Copy link
Copy Markdown
Member

Summary

The WebDAV endpoints take XML request bodies on PROPFIND, PROPPATCH and LOCK.
RFC 4918 defines those as plain namespaced documents, and no WebDAV client sends a
document type declaration, so there is no reason for us to accept one. This adds a
filter on /webdav/* that validates those bodies up front, returning 400 for
anything carrying a DTD, plus a size cap.

The check sits in a filter rather than in the parser because the WebDAV servlet comes
from a third-party library whose parsers are not configurable from our code, and one
of the three parses happens in a static method. A filter is the only place that
covers all three consistently.

Behaviour

  • Only PROPFIND, PROPPATCH and LOCK are inspected. Other methods are forwarded untouched.
  • A body with a DTD, or malformed XML, returns 400 rather than the 500 it used to produce.
  • Bodies over 512KB return 413. Validating a body means buffering it, so it needs a bound.
  • Empty bodies pass through unchanged: PROPFIND with no body means allprop, and LOCK with no body is a refresh.
  • The body is replayed to the servlet byte for byte, so nothing downstream sees a consumed stream.

Testing

Six unit tests in WebDavXmlValidationFilterTest cover rejection, pass-through with an
intact body, empty bodies, non-XML methods, the size cap, and detection across encodings.

Also exercised against a running instance: the existing WebDav Postman collection passes
all 14 of its requests, and PROPFIND (with and without a body), OPTIONS, MKCOL, PUT,
GET, PROPPATCH and LOCK all behave as they did before the change.

Closes dotCMS/private-issues#671

Add a filter on /webdav/* that parses PROPFIND, PROPPATCH and LOCK bodies
with disallow-doctype-decl and rejects any document carrying a document type
declaration with a 400. RFC 4918 bodies are plain namespaced XML, so no real
client sends a DTD.

The check has to sit in a filter rather than in the parser: the WebDAV servlet
comes from a third-party library whose parsers cannot be configured from here,
and one of the three parses happens in a static method.

Bodies are capped at 512KB, since validating one means buffering it. Empty
bodies are left alone, as PROPFIND with no body means allprop and LOCK with no
body is a refresh. The consumed body is replayed to the servlet intact.
@github-actions github-actions Bot added the Area : Backend PR changes Java/Maven backend code label Aug 24, 2026
@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Claude finished @swicken's task in 1m 45s —— View job


Code Review — WebDAV XML validation filter

I read WebDavXmlValidationFilter.java, its test, the web.xml mapping, and ThreadLocalSaxParserFactory. The design is sound: only PROPFIND/PROPPATCH/LOCK are buffered (uploads via PUT are correctly excluded, so large-file transfers aren't held in memory), the body is replayed byte-for-byte, and detection is a real parse so encoding tricks (UTF-16) can't hide a DTD.

New Issues

No blocking issues found.

Notes (non-blocking)

  • 🟡 WebDavXmlValidationFilter.java:161 — the "could not build a detecting parser" path still uses Logger.error, while the two request-rejection paths now correctly use SecurityLogger. This matches @wezell's "this should never happen in normal operations" comment; SecurityLogger.logError would keep all three abnormal-path logs on the same channel. Optional. Fix this →

Resolved

  • WebDavXmlValidationFilter.java:107,117@wezell's request to use the SecurityLogger for the reject paths is addressed; both the oversized (413) and DOCTYPE (400) rejections now call SecurityLogger.logWarn.
  • WebDavXmlValidationFilter.java:100 — method matching normalises with Locale.ROOT, and there's a Turkish-locale regression test covering it.
  • WebDavXmlValidationFilter.java:168 — Semgrep XXE finding is a false positive. ThreadLocalSaxParserFactory disables external general/parameter entities, external DTD loading, and enables secure processing before any request-controlled bytes are parsed; the parse aborts at startDTD and never resolves entities. The /fp reply is justified.

Verification of the correctness-critical claims

  • Stream replay: getInputStream() caches the stream and hands back the same instance, so a second reader can't silently re-read from byte zero — matched by the assertSame test.
  • Empty vs. oversized: readCappedBody returns an empty Optional only when over cap; a zero-length body returns Optional.of(new byte[0]) and is forwarded, so allprop PROPFIND and LOCK refresh still work.
  • Malformed bodies are forwarded unchanged rather than rejected, preserving the servlet's 207 allprop fallback.
  • No merge-base bug: getReader() defaults to ISO-8859-1 and throws UnsupportedEncodingException on a bad charset, per the servlet spec.

Nice work — this is a clean, well-documented change with good test coverage. The one note above is optional and doesn't block.

· issue-671-webdav-xml-request-validation

…factory

Detect the declaration through the lexical handler instead of relying on a
parse failure. The servlet tolerates a body it cannot parse -- a PROPFIND with
broken XML falls back to allprop and still answers 207 -- so failing those was
a behaviour change unrelated to DTDs. A malformed body is now forwarded
untouched, with a test covering it.

Build the reader from ThreadLocalSaxParserFactory rather than configuring a new
SAXParserFactory per request. Its factory already disables external entities
and external DTD loading while still reporting the declaration, which is what
detection needs, so this also drops a fourth copy of the feature block.

Make the size cap operator-tunable as WEBDAV_MAX_XML_BODY_BYTES, matching
BoundedBufferedReader and MultiPartSecurityRequestWrapper.

Fix getReader(): default to ISO-8859-1 as the servlet spec requires when the
request states no encoding, and translate a bad charset token into the checked
UnsupportedEncodingException the contract declares.

Drop the javadoc claim that this parser and the servlet's cannot disagree. They
resolve through independent mechanisms, JAXP here and XMLReaderFactory there.

Build the test request from DotCMSMockRequest instead of hand-rolling a mock
and a ServletInputStream.
The wrapper built a fresh replay stream per call, so a second caller would have
silently re-read the body from the start. A request body is read once: build it
lazily and cache it. This also matches the real container request, where a
second call returns the same, already-consumed stream.

Covered by asserting the same instance comes back twice and that the body is
not re-readable once consumed.
…ed read

The detection parse signalled a declaration by throwing a shared exception
instance and the caller compared identity. Any layer that wrapped what the
handler threw would have made that comparison miss, and the failure mode was a
DTD forwarded silently. Use a private exception type, catch broadly, and walk
the cause chain, bounded so a cyclic chain cannot spin.

Replace the hand-rolled capped read loop with InputStream.readNBytes: reading
one byte past the cap is enough to know it was exceeded.
…r the cause chain

toUpperCase() without a locale is a footgun: under a Turkish default locale
"propfind" uppercases to "PROPFIND" with a dotted capital I, which would not
match and would skip validation entirely. Pinned by a test that sets tr-TR and
fails without the fix.

Replace the hand-rolled cause-chain walk and its depth guard with
ExceptionUtils.indexOfType from commons-lang3, already a direct dependency. It
tolerates a cyclic chain, so the guard goes too.
@mbiuki
mbiuki self-requested a review August 24, 2026 14:37

@wezell wezell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use the security logger here - this should never happen in normal operations.

Comment thread dotCMS/src/main/java/com/dotcms/filters/WebDavXmlValidationFilter.java Outdated

@wezell wezell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good change

@wezell
wezell added this pull request to the merge queue Aug 24, 2026
Any commits made after this event will not be merged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants