CVE-2025-52888
Description
Allure 2 is the version 2.x branch of Allure Report, a multi-language test reporting tool. A critical XML External Entity (XXE) vulnerability exists in the xunit-xml-plugin used by Allure 2 prior to version 2.34.1. The plugin fails to securely configure the XML parser (DocumentBuilderFactory) and allows external entity expansion when processing test result .xml files. This allows attackers to read arbitrary files from the file system and potentially trigger server-side request forgery (SSRF). Version 2.34.1 contains a patch for the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
io.qameta.allure.plugins:xunit-xml-pluginMaven | < 2.34.1 | 2.34.1 |
io.qameta.allure.plugins:junit-xml-pluginMaven | < 2.34.1 | 2.34.1 |
io.qameta.allure.plugins:trx-pluginMaven | < 2.34.1 | 2.34.1 |
Affected products
1- Range: 2.0-BETA1, 2.0-BETA3, 2.0-BETA4, …
Patches
27d47f0f555cccbcb33719851fix xee (via #3029)
8 files changed · +252 −0
allure-plugin-api/src/main/java/io/qameta/allure/parser/ClasspathEntityResolver.java+100 −0 added@@ -0,0 +1,100 @@ +/* + * Copyright 2016-2024 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.parser; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * ClasspathEntityResolver only allows to resolve dtd schemas, available in classpath. + * For all other entities it returns empty schema. + * + * @author charlie (Dmitry Baev). + */ +public class ClasspathEntityResolver implements EntityResolver { + + private static final Logger LOGGER = LoggerFactory.getLogger(ClasspathEntityResolver.class); + + private static final Pattern PATTERN = Pattern.compile("^.*/(.+\\.[dD][tT][dD]])$"); + + private static final byte[] EMPTY_SCHEMA_BYTES = "".getBytes(StandardCharsets.UTF_8); + + @Override + public InputSource resolveEntity(final String publicId, + final String systemId) { + if (Objects.nonNull(systemId)) { + final Matcher matcher = PATTERN.matcher(systemId); + if (matcher.matches()) { + final String schemaName = matcher.group(); + final String resourceName = "dtd/" + schemaName; + + return classpathInputSource(publicId, systemId, resourceName); + } + } + + return getInputSource(publicId, systemId, EMPTY_SCHEMA_BYTES); + } + + private InputSource classpathInputSource(final String publicId, + final String systemId, + final String resourceName) { + final byte[] schema = getBytes(resourceName); + return getInputSource(publicId, systemId, schema); + } + + private static InputSource getInputSource(final String publicId, final String systemId, final byte[] schema) { + final InputSource inputSource = new InputSource(new ByteArrayInputStream(schema)); + inputSource.setPublicId(publicId); + inputSource.setSystemId(systemId); + inputSource.setEncoding(StandardCharsets.UTF_8.name()); + return inputSource; + } + + @SuppressWarnings("AssignmentInOperand") + private static byte[] getBytes(final String resourceName) { + try (InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName)) { + if (Objects.isNull(is)) { + LOGGER.debug("schema resource {} not found", resourceName); + return EMPTY_SCHEMA_BYTES; + } + final byte[] buffer = new byte[1000]; + + final ByteArrayOutputStream byteArrayOutputStream + = new ByteArrayOutputStream(); + + int temp; + + while ((temp = is.read(buffer)) != -1) { + byteArrayOutputStream.write(buffer, 0, temp); + } + return byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + LOGGER.debug("can't read schema resource {}", resourceName, e); + return EMPTY_SCHEMA_BYTES; + } + } +}
gradle/quality-configs/pmd/pmd.xml+1 −0 modified@@ -97,6 +97,7 @@ <rule ref="category/java/errorprone.xml"> <exclude name="AvoidCatchingThrowable"/> <exclude name="AvoidFieldNameMatchingMethodName"/> + <exclude name="AssignmentInOperand"/> <!-- duplicate of checkstyle check AssignmentInOperand --> <exclude name="AvoidLiteralsInIfCondition"/> <exclude name="EmptyFinalizer"/> <exclude name="FinalizeDoesNotCallSuperFinalize"/>
plugins/junit-xml-plugin/src/main/java/io/qameta/allure/junitxml/JunitXmlPlugin.java+3 −0 modified@@ -30,6 +30,7 @@ import io.qameta.allure.entity.Step; import io.qameta.allure.entity.TestResult; import io.qameta.allure.entity.Time; +import io.qameta.allure.parser.ClasspathEntityResolver; import io.qameta.allure.parser.XmlElement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -133,7 +134,9 @@ private void parseRootElement(final Path resultsDirectory, final Path parsedFile try { LOGGER.debug("Parsing file {}", parsedFile); final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); final DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setEntityResolver(new ClasspathEntityResolver()); final XmlElement rootElement = new XmlElement(builder.parse(parsedFile.toFile()).getDocumentElement()); final String elementName = rootElement.getName();
plugins/junit-xml-plugin/src/test/java/io/qameta/allure/junitxml/JunitXmlPluginTest.java+37 −0 modified@@ -35,6 +35,7 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.ZoneOffset; @@ -349,6 +350,42 @@ void shouldProcessFilesWithZuluTimestamp() throws Exception { ); } + @Test + void cveEntityReadTest(@TempDir final Path tmp) throws IOException { + final Path secretFile = tmp.resolve("secretfile.ini"); + Files.writeString(secretFile, + "[owner]\n" + + "name = John Doe\n" + + "organization = Example Org.\n", + StandardCharsets.UTF_8 + ); + + Files.writeString( + resultsDirectory.resolve("bad-test.xml"), + "<?xml version=\"1.0\"?>\n" + + "<!DOCTYPE foo [\n" + + " <!ENTITY xxe SYSTEM \"" + secretFile.toUri() + "\">\n" + + "]>\n" + + "<testsuite tests=\"5\" failures=\"1\" name=\"org.allurefw.report.junit.JunitXmlPluginTest\" time=\"0.354\" errors=\"0\" skipped=\"1\">\n" + + " <testcase classname=\"org.allurefw.report.junit.JunitXmlPluginTest\" name=\"shouldReadFailures\" time=\"0.012\">\n" + + " <failure message=\"message\">&xxe;\n" + + " </failure>\n" + + " </testcase>\n" + + "</testsuite>", + StandardCharsets.UTF_8 + ); + + JunitXmlPlugin reader = new JunitXmlPlugin(); + + reader.readResults(configuration, visitor, resultsDirectory); + + final ArgumentCaptor<TestResult> captor = ArgumentCaptor.captor(); + verify(visitor, times(1)).visitTestResult(captor.capture()); + + final TestResult testResult = captor.getValue(); + assertThat(testResult.getStatusTrace()).doesNotContain("John Doe").doesNotContain("Example Org"); + } + private void process(String... strings) throws IOException { Iterator<String> iterator = Arrays.asList(strings).iterator(); while (iterator.hasNext()) {
plugins/trx-plugin/src/main/java/io/qameta/allure/trx/TrxPlugin.java+3 −0 modified@@ -25,6 +25,7 @@ import io.qameta.allure.entity.Step; import io.qameta.allure.entity.TestResult; import io.qameta.allure.entity.Time; +import io.qameta.allure.parser.ClasspathEntityResolver; import io.qameta.allure.parser.XmlElement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -106,7 +107,9 @@ protected void parseTestRun(final Path parsedFile, final RandomUidContext contex LOGGER.debug("Parsing file {}", parsedFile); final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); final DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setEntityResolver(new ClasspathEntityResolver()); final Document document = builder.parse(parsedFile.toFile()); final XmlElement testRunElement = new XmlElement(document.getDocumentElement()); final String elementName = testRunElement.getName();
plugins/trx-plugin/src/test/java/io/qameta/allure/trx/TrxPluginTest.java+61 −0 modified@@ -30,6 +30,7 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -183,6 +184,66 @@ void shouldParseTestResultChildren() throws Exception { } + @Test + void cveEntityReadTest(@TempDir final Path tmp) throws IOException { + final Path secretFile = tmp.resolve("secretfile.ini"); + Files.writeString(secretFile, + "[owner]\n" + + "name = John Doe\n" + + "organization = Example Org.\n", + StandardCharsets.UTF_8 + ); + + Files.writeString( + resultsDirectory.resolve("bad-test.trx"), + "<?xml version=\"1.0\"?>\n" + + "<!DOCTYPE foo [\n" + + " <!ENTITY xxe SYSTEM \"" + secretFile.toUri() + "\">\n" + + "]>\n" + + "<TestRun id=\"37bd1bbc-784e-477a-8fe2-a116517ba93f\" name=\"@ip-10-0-12-95 2017-09-11 07:32:55\" xmlns=\"http://microsoft.com/schemas/VisualStudio/TeamTest/2010\">\n" + + " <Times creation=\"2017-09-11T07:32:55.5710479+00:00\" queuing=\"2017-09-11T07:32:55.5710493+00:00\" start=\"2017-09-11T07:31:47.7161493+00:00\" finish=\"2017-09-11T07:50:48.6048416+00:00\" />\n" + + " <TestSettings name=\"default\" id=\"fec1d7e5-efa1-43b5-b261-7507a1de835f\">\n" + + " <Deployment runDeploymentRoot=\"_ip-10-0-12-95 2017-09-11 07_32_55\" />\n" + + " </TestSettings>\n" + + " <Results>\n" + + " <UnitTestResult executionId=\"a0c122ad-b99a-4e42-b42c-9f03a42e789d\" testId=\"6efcec51-ecd8-464a-afdf-a8f254074a1a\" testName=\"MyCompany.TestSuite.IntegrationTests.Retrieve.RetrieveTestCases.Test_BookingIdInResponse_Succeeds\" computerName=\"ip-10-0-12-95\" duration=\"00:00:00.0010000\" startTime=\"2017-09-11T07:32:55.4659624+00:00\" endTime=\"2017-09-11T07:32:55.4659679+00:00\" testType=\"13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b\" outcome=\"Failed\" testListId=\"8c84fa94-04c1-424b-9868-57a2d4851a1d\" relativeResultsDirectory=\"a0c122ad-b99a-4e42-b42c-9f03a42e789d\">\n" + + " <Output>\n" + + " <ErrorInfo>\n" + + " <Message>&xxe;</Message>\n" + + " <StackTrace>&xxe;</StackTrace>\n" + + " </ErrorInfo>\n" + + " </Output>\n" + + " </UnitTestResult>\n" + + " </Results>\n" + + " <TestDefinitions>\n" + + " <UnitTest name=\"MyCompany.TestSuite.IntegrationTests.Retrieve.RetrieveTestCases.Test_BookingIdInResponse_Succeeds\" storage=\"/var/lib/jenkins/workspace/Product/code/test/MyCompany.testsuite/bin/release/netcoreapp2.0/MyCompany.testsuite.dll\" id=\"6efcec51-ecd8-464a-afdf-a8f254074a1a\">\n" + + " <Execution id=\"a0c122ad-b99a-4e42-b42c-9f03a42e789d\" />\n" + + " <TestMethod codeBase=\"/var/lib/jenkins/workspace/Product/code/test/MyCompany.TestSuite/bin/Release/netcoreapp2.0/MyCompany.TestSuite.dll\" executorUriOfAdapter=\"executor://xunit/VsTestRunner2\" className=\"MyCompany.TestSuite.IntegrationTests.Retrieve.RetrieveTestCases\" name=\"MyCompany.TestSuite.IntegrationTests.Retrieve.RetrieveTestCases.Test_BookingIdInResponse_Succeeds\" />\n" + + " </UnitTest>\n" + + " </TestDefinitions>\n" + + " <TestEntries>\n" + + " <TestEntry testId=\"6efcec51-ecd8-464a-afdf-a8f254074a1a\" executionId=\"a0c122ad-b99a-4e42-b42c-9f03a42e789d\" testListId=\"8c84fa94-04c1-424b-9868-57a2d4851a1d\" />\n" + + " </TestEntries>\n" + + " <TestLists>\n" + + " <TestList name=\"Results Not in a List\" id=\"8c84fa94-04c1-424b-9868-57a2d4851a1d\" />\n" + + " <TestList name=\"All Loaded Results\" id=\"19431567-8539-422a-85d7-44ee4e166bda\" />\n" + + " </TestLists>\n" + + "</TestRun>", + StandardCharsets.UTF_8 + ); + + TrxPlugin reader = new TrxPlugin(); + + reader.readResults(configuration, visitor, resultsDirectory); + + final ArgumentCaptor<TestResult> captor = ArgumentCaptor.captor(); + verify(visitor, times(1)).visitTestResult(captor.capture()); + + final TestResult testResult = captor.getValue(); + assertThat(testResult.getStatusMessage()).doesNotContain("John Doe"); + assertThat(testResult.getStatusTrace()).doesNotContain("Example Org"); + } + private void process(String... strings) throws IOException { Iterator<String> iterator = Arrays.asList(strings).iterator(); while (iterator.hasNext()) {
plugins/xunit-xml-plugin/src/main/java/io/qameta/allure/xunitxml/XunitXmlPlugin.java+3 −0 modified@@ -23,6 +23,7 @@ import io.qameta.allure.entity.Status; import io.qameta.allure.entity.TestResult; import io.qameta.allure.entity.Time; +import io.qameta.allure.parser.ClasspathEntityResolver; import io.qameta.allure.parser.XmlElement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -94,7 +95,9 @@ private void parseAssemblies(final Path parsedFile, final RandomUidContext conte try { LOGGER.debug("Parsing file {}", parsedFile); final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); final DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setEntityResolver(new ClasspathEntityResolver()); final Document document = builder.parse(parsedFile.toFile()); final XmlElement assembliesElement = new XmlElement(document.getDocumentElement()); final String elementName = assembliesElement.getName();
plugins/xunit-xml-plugin/src/test/java/io/qameta/allure/xunitxml/XunitXmlPluginTest.java+44 −0 modified@@ -34,6 +34,7 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; @@ -185,6 +186,49 @@ void shouldSetStatusDetails(final String resource, ); } + @Test + void cveEntityReadTest(@TempDir final Path tmp) throws IOException { + final Path secretFile = tmp.resolve("secretfile.ini"); + Files.writeString(secretFile, + "[owner]\n" + + "name = John Doe\n" + + "organization = Example Org.\n", + StandardCharsets.UTF_8 + ); + + Files.writeString( + resultsDirectory.resolve("bad-test.xml"), + "<?xml version=\"1.0\"?>\n" + + "<!DOCTYPE foo [\n" + + " <!ENTITY xxe SYSTEM \"" + secretFile.toUri() + "\">\n" + + "]>\n" + + "<assemblies>\n" + + " <assembly test-framework=\"xunit\">\n" + + " <collection>\n" + + " <test name=\"Exploit Test\" method=\"testMethod\" type=\"TestClass\" result=\"Fail\">\n" + + " <failure>\n" + + " <message>&xxe;</message>\n" + + " <stack-trace>Trace with &xxe;</stack-trace>\n" + + " </failure>\n" + + " </test>\n" + + " </collection>\n" + + " </assembly>\n" + + "</assemblies>", + StandardCharsets.UTF_8 + ); + + XunitXmlPlugin reader = new XunitXmlPlugin(); + + reader.readResults(configuration, visitor, resultsDirectory); + + final ArgumentCaptor<TestResult> captor = ArgumentCaptor.captor(); + verify(visitor, times(1)).visitTestResult(captor.capture()); + + final TestResult testResult = captor.getValue(); + assertThat(testResult.getStatusMessage()).doesNotContain("John Doe"); + assertThat(testResult.getStatusTrace()).doesNotContain("Example Org"); + } + private void process(String... strings) throws IOException { Iterator<String> iterator = Arrays.asList(strings).iterator(); while (iterator.hasNext()) {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.