matrei commented on PR #16310:
URL: https://github.com/apache/grails-core/pull/16310#issuecomment-5598308166
# AI Review Findings
Head `6fdc50880a` on `8.0.x` base `55d5076aa5`. I reproduced the premise
independently against the JDK 21 parser
(`com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl`): every
`http://` identifier is accepted and every `https://` spelling is answered with
`SAXNotRecognizedException`. The sweep that introduced the `https` forms is
`eed8df3594` (#13478, 79 files), so since April 2024 `createParserFactory()`
has configured nothing beyond `FEATURE_SECURE_PROCESSING`, and the `catch
(Exception)` around each call kept that invisible. The correction is real and
the direction is right.
There is one blocking problem with the default this PR ships, and it is not
covered by any test in the repository. The follow-up in
jamesfredley/grails-core#4 addresses it with a global opt-in; I would take that
PR with one change in shape (see the review on jamesfredley/grails-core#4).
## [P1] Rejecting DOCTYPE in the shared factory breaks JSP tag library
resolution for every application that uses JSTL
**Files:**
-
`grails-gradle/model/src/main/groovy/org/grails/io/support/SpringIOUtils.java:433`
-
`grails-gsp/grails-web-jsp/src/main/groovy/org/grails/gsp/jsp/TldReader.groovy:49`
-
`grails-gsp/grails-web-jsp/src/main/groovy/org/grails/gsp/jsp/TagLibraryResolverImpl.groovy:71-90`
-
`grails-gsp/spring-boot/src/main/java/grails/gsp/boot/GspAutoConfiguration.java:323`
`createParserFactory()` returns one cached factory for every caller of
`createXmlSlurper()` and `newSAXParser()`. Those callers fall into two trust
levels:
| Caller | Input |
|---|---|
| `XmlDataBindingSourceCreator`, `HalXmlDataBindingSourceCreator`,
`grails.converters.XML.parse` | HTTP request bodies (untrusted) |
| `TldReader`, `WebXmlTagLibraryReader`, `PluginUtils` | Descriptors on the
application classpath (trusted) |
The descriptors are where the strict default bites. `GspAutoConfiguration`
defaults `grails.gsp.tldScanPattern` to a list that ends with
`classpath*:/META-INF/c-1_0-rt.tld`. In
`org.glassfish.web:jakarta.servlet.jsp.jstl:3.0.1` that file opens with a JSP
1.2 DOCTYPE, and so do 8 of the 22 descriptors in the jar (`c-1_0*.tld`,
`fmt-1_0*.tld`, `sql-1_0*.tld`, `x-1_0*.tld`).
`TagLibraryResolverImpl.initialize()` scans every pattern in one loop and
catches nothing, so the first `resolveTagLibrary(uri)` call throws
`SAXParseException: DOCTYPE is disallowed ...` and no JSP tag library resolves,
including `jakarta.tags.core` from the DOCTYPE-free `c.tld` that was scanned
earlier in the same loop.
I confirmed this with a spec in `grails-gsp/plugin` (JSTL is already on that
module's test runtime classpath) that scans `c-1_0-rt.tld` plus `c.tld` and
resolves `jakarta.tags.core`. On this head it fails with `DOCTYPE is
disallowed`. The existing GSP tests stay green only because
`GroovyPageWithJSPTagsTests`, `AbstractGrailsTagTests` and
`TagLibraryResolverTests` scan `c.tld`, `fmt.tld`, `core.tld` and
`spring*.tld`, none of which carries a DOCTYPE; `TldReaderTests` uses a fixture
without one. The two test examples that put JSTL on the runtime classpath
(`gsp-layout`, `gsp-sitemesh3`) contain no `<%@ taglib %>` directive, so the
resolver is never initialised there either.
The documented custom pattern `classpath*:/META-INF/*.tld` in
`usingJSPTagLibraries.adoc` hits all eight.
The user-visible symptom is the worst kind: a 500 on the first page that
uses a JSP tag, with a parser error that names a feature URI nobody set in the
application.
**What I would change.** The two trust levels want two parsers, and the
readers of trusted descriptors know they are reading trusted descriptors. Keep
the strict default for request bodies and let the descriptor readers ask for
DOCTYPE tolerance explicitly:
```java
// SpringIOUtils
public static XmlSlurper createXmlSlurper() throws ... { //
strict, for request bodies
return createXmlSlurper(false);
}
public static XmlSlurper createXmlSlurper(boolean allowDocTypeDeclaration)
throws ... {
return new
XmlSlurper(createParserFactory(allowDocTypeDeclaration).newSAXParser());
}
```
with `TldReader`, `WebXmlTagLibraryReader` and `PluginUtils` passing `true`.
External general and parameter entities, DTD grammar loading and external DTD
retrieval stay off on both factories, so the tolerant parser still resolves a
`file://` entity to nothing and skips the `web-jsptaglibrary_1_2.dtd` reference
instead of fetching it. That needs no configuration key, no upgrade note for
JSP users, and does not relax request-body parsing as a side effect of using
JSTL.
jamesfredley/grails-core#4 instead adds
`grails.xml.allowDocTypeDeclaration`, read through `Metadata`, that switches
the single shared factory. It works, and I ran its tests, but it makes "I use
JSTL" and "accept DOCTYPE in request bodies" the same switch. Details in that
review.
## [P2] A rejected hardening feature is still swallowed silently
**Files:**
-
`grails-gradle/model/src/main/groovy/org/grails/io/support/SpringIOUtils.java:431-461`
-
`grails-testing-support-http-client/src/main/groovy/org/apache/grails/testing/http/client/utils/XmlUtils.groovy:235-243`
The PR exists because `catch (Exception) { /* ignore */ }` hid a
`SAXNotRecognizedException` for two years, and the catch blocks are unchanged.
The tolerance for parsers lacking a feature is reasonable, but it should leave
a trace. `grails-gradle-model` already has `slf4j-api` as an `api` dependency;
a `LOG.warn("Parser {} does not support feature {}", factory.getClass(), name)`
inside each catch costs nothing and would have surfaced this in every
application log since 2024. Copilot's suggestion to make
`disallow-doctype-decl` mandatory and propagate is too strong for a shared
factory, but a warning is not.
jamesfredley/grails-core#4 adds `XmlParserFeatureSpec`, which asserts each
identifier is recognised by the JDK parser. That pins the identifiers at test
time; the warning covers the runtime case where a different SAX provider is
first on the classpath.
## [P2] The four external-entity features lose their only behavioural
coverage
**Files:**
-
`grails-gradle/model/src/test/groovy/org/grails/io/support/SpringIOUtilsSpec.groovy`
-
`grails-testing-support-http-client/src/test/groovy/org/apache/grails/testing/http/client/utils/XmlUtilsSpec.groovy:279-300`
-
`grails-testing-support-http-client/src/test/groovy/org/apache/grails/testing/http/client/TestHttpResponseSpec.groovy:201-227`
Every new test asserts DOCTYPE rejection, and once a DOCTYPE is refused no
document reaches `external-general-entities`, `external-parameter-entities`,
`load-dtd-grammar` or `load-external-dtd`. The deleted `newXmlSlurper blocks
external entities` and `xml uses a secure default slurper that does not resolve
external entities` were the only tests that drove an entity through the parser
and asserted the file contents did not appear. After this PR the same wrong
identifier in any of those four lines passes CI again.
jamesfredley/grails-core#4 restores this coverage for `SpringIOUtils` by
testing with DOCTYPE permitted; the split-factory shape above makes that
natural, because the tolerant factory is the one that can be tested for entity
blocking.
## [P2] `THREAT_MODEL.md` §9 now states something the code no longer does
**Files:**
- `THREAT_MODEL.md:346`
- `THREAT_MODEL.md:470`
The description says "No threat-model change in this PR", but §9 currently
reads "XXE in XML data binding. XML parsing is delegated to the underlying
parser; the framework does not impose a parser configuration. *(inferred)*",
and §14 question 13 proposes confirming exactly that. After this PR the
framework does impose a configuration on every XML request body it binds, and
the strict DOCTYPE default is a behaviour change users will hit. The sentence
in §9 should move to §8 or be rewritten to describe what is now guaranteed
(external entities and external DTDs refused, DOCTYPE refused unless opted in),
and Q13 should be answered rather than left open. Leaving the disclaimer in
place means the next security review will re-report f002 against a document
that says the fix does not exist.
Related: the upgrade notes need to say that `application/xml`, `text/xml`
and HAL XML request bodies carrying a DOCTYPE are now refused.
`integrationTesting.adoc` only covers the test client.
jamesfredley/grails-core#4 adds an "XML Parsing Defaults" section to
`upgrading.adoc` that does this.
## [P3] Small things in the test client, still present on
jamesfredley/grails-core#4's head
- `XmlUtils.groovy:116` javadoc drops "disables external entity expansion
plus external DTD loading" although those features are still set; the README
and `integrationTesting.adoc` kept the sentence.
- `XmlUtilsSpec.groovy:281,292`: `def parsed =` is assigned and never read.
- `TestHttpResponseSpec.groovy:201-227` and `XmlUtilsSpec.groovy:279-300`:
both tests now assert a bare `thrown(SAXParseException)`, which a malformed
document also satisfies. `e.message.contains('DOCTYPE is disallowed')` pins
them to the behaviour their names claim.
- The external-entity fixtures indent `<!ENTITY` and `]>` by one space; the
internal-entity fixtures next to them do not.
## Verified
- Feature recognition probe against JDK 21: 5/5 `http://` accepted, 5/5
`https://` rejected with `SAXNotRecognizedException`.
- JSTL 3.0.1 jar: 8 of 22 `.tld` files contain `<!DOCTYPE`; `c-1_0-rt.tld`
(in the default scan pattern) is one of them, uri
`http://java.sun.com/jstl/core_rt`.
- No `https://apache.org/xml/features` or `https://xml.org/sax/features`
strings remain anywhere in the tree after this PR. The other `http://` sites
(`GrailsUpdater`, `GrailsViolationAggregationPlugin`,
`grails-test-report/build.gradle`) were already correct.
- `:grails-gsp:test` with a two-case spec scanning `c-1_0-rt.tld` plus
`c.tld` (run on jamesfredley/grails-core#4's head, whose strict default is
identical to this PR's): `resolveTagLibrary('jakarta.tags.core')` throws
`SAXParseException` containing `DOCTYPE is disallowed`; with
jamesfredley/grails-core#4's opt-in both `jakarta.tags.core` and
`http://java.sun.com/jstl/core_rt` resolve.
- `:grails-gradle-model:test`, `:grails-gradle-common:test`,
`:grails-testing-support-http-client:test` and the matching `codeStyle` tasks
on jamesfredley/grails-core#4's head: 173 cases, all green.
- Commit-level checks on `6fdc50880a` show only CodeQL and the release
drafter; the PR workflow runs are what CI reports, and none of them scans a
DOCTYPE-bearing TLD.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]