XMPMetaParserSecurityTest.java
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2025 Apryse Group NV
Authors: Apryse Software.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itextpdf.kernel.xmp.impl;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.utils.DefaultSafeXmlParserFactory;
import com.itextpdf.kernel.utils.XmlProcessorCreator;
import com.itextpdf.kernel.xmp.XMPException;
import com.itextpdf.test.ExtendedITextTest;
import com.itextpdf.test.ExceptionTestUtil;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Tag;
@Tag("UnitTest")
public class XMPMetaParserSecurityTest extends ExtendedITextTest {
private static final String XMP_WITH_XXE = "<?xpacket begin=\"\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n"
+ "<!DOCTYPE foo [ <!ENTITY xxe SYSTEM \"xxe-data.txt\" > ]>\n"
+ "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\">\n"
+ " <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n"
+ " <rdf:Description rdf:about=\"\" xmlns:pdfaid=\"http://www.aiim.org/pdfa/ns/id/\">\n"
+ " <pdfaid:part>&xxe;1</pdfaid:part>\n"
+ " <pdfaid:conformance>B</pdfaid:conformance>\n"
+ " </rdf:Description>\n"
+ " </rdf:RDF>\n"
+ "</x:xmpmeta>\n"
+ "<?xpacket end=\"r\"?>";
private static final String DTD_EXCEPTION_MESSAGE = ExceptionTestUtil.getDoctypeIsDisallowedExceptionMessage();
@BeforeEach
public void resetXmlParserFactoryToDefault() {
XmlProcessorCreator.setXmlParserFactory(null);
}
@Test
public void xxeTestFromString() throws XMPException {
Exception e = Assertions.assertThrows(XMPException.class, () -> XMPMetaParser.parse(XMP_WITH_XXE, null));
Assertions.assertEquals(DTD_EXCEPTION_MESSAGE, e.getMessage());
}
@Test
public void xxeTestFromByteBuffer() throws XMPException {
Exception e = Assertions.assertThrows(XMPException.class,
() -> XMPMetaParser.parse(XMP_WITH_XXE.getBytes(StandardCharsets.UTF_8), null)
);
Assertions.assertEquals(DTD_EXCEPTION_MESSAGE, e.getMessage());
}
@Test
public void xxeTestFromInputStream() throws XMPException, IOException {
try (InputStream inputStream = new ByteArrayInputStream(XMP_WITH_XXE.getBytes(StandardCharsets.UTF_8))) {
Exception e = Assertions.assertThrows(XMPException.class,
() -> XMPMetaParser.parse(inputStream, null)
);
Assertions.assertEquals(DTD_EXCEPTION_MESSAGE, e.getMessage());
}
}
@Test
public void xxeTestFromStringCustomXmlParser() throws XMPException {
XmlProcessorCreator.setXmlParserFactory(new SecurityTestXmlParserFactory());
Exception e = Assertions.assertThrows(PdfException.class,
() -> XMPMetaParser.parse(XMP_WITH_XXE, null)
);
Assertions.assertEquals("Test message", e.getMessage());
}
}