We have nested xml stylesheets that get read by the native Java XML parser / transformer.
<!-- stylesheets/subfolder/stylesheet.xsl -->
<axsl:stylesheet xmlns:axsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<axsl:variable name="generalCodes" select="document('GeneralCodeLists.xsd')"/>
...
/>
import org.w3c.Document;
import javax.xml.*;
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
Document sourceDoc = dbf.newDocumentBuilder.newDocument();
Document resultDoc = dbf.newDocumentBuilder.newDocument();
TransformerFactory factory = TransformerFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "file");
StreamSource source = new StreamSource((new ClassPathResource("stylesheets/subfolder/stylesheet.xsl")).getInputStream());
Templates templates = factory.newTemplates(source);
Transformer transformer = templates.newTransformer();
transformer.transform(new DOMSource(sourceDoc), new DOMResult(resultDoc));
This throws the following exception
java.lang.Exception: Could not read stylesheet target 'GeneralCodeLists.xsd' because 'nested' access is not allowed due to restriction set by the accessExternalStylesheet property.
Setting the loaderImplementation to CLASSIC fixes the problem.
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<loaderImplementation>CLASSIC</loaderImplementation>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Comment From: mhalbritter
Thanks for the report. Can you please create a minimal reproducer and provide it to us?
Comment From: nmesot
Hi! Sure, here is a working example. Running instructions:
- Unzip
- Run
mvn clean package - Run
java -jar target/app.jar - Run
curl localhost:8080/test
-> observe error
- Uncomment
<loaderImplementation>CLASSIC</loaderImplementation>in pom.xml (line 40) - Run
mvn clean package - Run
java -jar target/app.jar - Run
curl localhost:8080/test
-> observe that error is gone
Do not run from intelIJ directly, since intelIJ does not build the "Uber Jar".
Java version:
openjdk 17.0.6 2023-01-17
OpenJDK Runtime Environment Temurin-17.0.6+10 (build 17.0.6+10)
OpenJDK 64-Bit Server VM Temurin-17.0.6+10 (build 17.0.6+10, mixed mode, sharing)
Maven version:
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Java version: 17.0.6, vendor: Eclipse Adoptium, runtime: c:\srdev\lds\jdk\jdk-17.0.6+10
Default locale: en_US, platform encoding: Cp1252
OS name: "windows 10", version: "10.0", arch: "amd64", family: "windows"
Note: The code provided is as close to our production setup as possible. Because of various reasons, changing parameters for the Transformer and DocumentBuilderFactory classes is not possible.
Comment From: wilkinsona
Thanks for the sample.
The problem is the nested URI scheme that's used by the new loader implementation. The URI for XSD is something like jar:nested:/Users/awilkinson/Downloads/spring-boot-bug-report/target/app.jar/!BOOT-INF/classes/!/stylesheets/subfolder1/subfolder2/TestLists.xsd. This isn't permitted due the factory configuration:
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "file");
Right now, I can't see any obvious way around this other than changing the configuration of the factory and I note that you've said that's not possible for you.
Comment From: wilkinsona
@nmesot when you say "parameters", does that mean you can't change anything at all on the TransformerFactory? The problem does not occur if you configure a URIResolver like this:
factory.setURIResolver(new URIResolver() {
@Override
public Source resolve(String href, String base) throws TransformerException {
try {
URLConnection connection = new URL(base).openConnection();
try (InputStream input = connection.getInputStream()) {
return new ResourceSource(new ByteArrayResource(StreamUtils.copyToByteArray(input)));
}
}
catch (Exception ex) {
throw new TransformerException(ex);
}
}
});
Comment From: johannesboon
Thank you for the investigation and workaround, which we have also successfully applied to our application.
Setting the loaderImplementation to CLASSIC fixes the problem.
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> <configuration> <loaderImplementation>CLASSIC</loaderImplementation> </configuration> </execution> </executions> </plugin> </plugins> </build>
Some background/introduction of the change, in case anyone missed it:
"The underlying code that supports Spring Boot’s "Uber Jar" loading has been rewritten now that we no longer need to support Java 8. The updated code makes use of a new URL format which is more compliant with JDK expectations. The previous URL format of jar:file:/dir/myjar.jar:BOOT-INF/lib/nested.jar!/com/example/MyClass.class has been replaced with jar:nested:/dir/myjar.jar/!BOOT-INF/lib/nested.jar!/com/example/MyClass.class "
Source: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes#nested-jar-support
Our application uses org.springframework.ws.soap.server.endpoint.interceptor.PayloadValidatingInterceptor that is similarly broken.
Comment From: wilkinsona
Thanks, @johannesboon. Are you a colleague of @nmesot or someone else affected by the same problem?
I'm not very familiar with Spring Web Services and it's not clear to me how PayloadValidatingInterceptor would be affected. If possible, could you provide a minimal sample that shows it breaking?
Comment From: johannesboon
I think I'm working for a different employer and customer, so "someone else" who is also affected by the same change.
We use https://docs.spring.io/spring-ws/docs/current/api/org/springframework/ws/config/annotation/WsConfigurer.html#addInterceptors(java.util.List) that takes instances of e.g.: https://docs.spring.io/spring-ws/docs/current/api/org/springframework/ws/soap/server/endpoint/interceptor/PayloadValidatingInterceptor.html
E.g. like this:
@Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
PayloadValidatingInterceptor validatingInterceptor = new PayloadValidatingInterceptor();
validatingInterceptor.setValidateRequest(true);
validatingInterceptor.setValidateResponse(true);
validatingInterceptor.setXsdSchema(new SimpleXsdSchema(new ClassPathResource("schemas/ourschema.xsd"););
interceptors.add(validatingInterceptor);
}
Where schemas/ourschema.xsd is a resource the classloader should find in a maven dependency, that is included as jar within the uber jar.
Another example can be found in: https://github.com/spring-projects/spring-ws-samples/blob/main/echo/server/src/main/java/org/springframework/ws/samples/echo/config/EchoConfig.java although that might not brake as it uses Spring Boot 3.0 and the Schema is included within the same project, so it does not need to be retrieved from a nested jar.
Comment From: nmesot
Nice to see that I am not the only one using xml stylesheets to validate payloads :D.
Configuring the URIResolver did the trick, thank you!
Comment From: mdehmel
I think I'm working for a different employer and customer, so "someone else" who is also affected by the same change.
We use https://docs.spring.io/spring-ws/docs/current/api/org/springframework/ws/config/annotation/WsConfigurer.html#addInterceptors(java.util.List) that takes instances of e.g.: https://docs.spring.io/spring-ws/docs/current/api/org/springframework/ws/soap/server/endpoint/interceptor/PayloadValidatingInterceptor.html
E.g. like this:
java @Override public void addInterceptors(List<EndpointInterceptor> interceptors) { PayloadValidatingInterceptor validatingInterceptor = new PayloadValidatingInterceptor(); validatingInterceptor.setValidateRequest(true); validatingInterceptor.setValidateResponse(true); validatingInterceptor.setXsdSchema(new SimpleXsdSchema(new ClassPathResource("schemas/ourschema.xsd");); interceptors.add(validatingInterceptor); }Where
schemas/ourschema.xsdis a resource the classloader should find in a maven dependency, that is included as jar within the uber jar.
I am facing the same issue with the described PayloadValidatingInterceptor approach.
The call to validatingInterceptor.setXsdSchema(...) is leading to the instantiation of a new XmlValidator object. This one is using the following call to load the schema. And that's where the error is coming from.
SchemaLoaderUtils. loadSchema(new ClassPathResource("schemas/ourschema.xsd"), "http://www.w3.org/2001/XMLSchema" );
The org.springframework.xml.validation.SchemaLoaderUtils uses the org.springframework.xml.validation.SchemaFactoryUtils to create a new SchemaFactory. And there is also a configuration applied:
schemaFactory.setProperty("http://javax.xml.XMLConstants/property/accessExternalSchema", "file,jar:file,wsjar");
When I change the property value as follows, I am able to run loadSchema successfully:
schemaFactory.setProperty("http://javax.xml.XMLConstants/property/accessExternalSchema", "file,jar:file,wsjar,nested");
I am not really into this schema loading thing. So I cannot say if this is a valid and secure approach. But at least it shows where the problem seems to be coming from.
Comment From: wilkinsona
Thanks very much, @mdehmel. I've opened https://github.com/spring-projects/spring-ws/issues/1393 to update org.springframework.xml.validation.SchemaFactoryUtils so that jar:nested is permitted by default.
I don't think there's anything more that we can do here for this one. If the URIResolver workaround is not suitable or until the Spring WS issue has been addressed, an alternative is to configure your uber jar to unpack the archive that contains the external resources.