TestSchemaExternalEntity.java
package wstxtest.vstream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import javax.xml.stream.XMLStreamException;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import org.codehaus.stax2.XMLStreamReader2;
import org.codehaus.stax2.validation.XMLValidationException;
import org.codehaus.stax2.validation.XMLValidationSchema;
import org.codehaus.stax2.validation.XMLValidationSchemaFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import com.ctc.wstx.api.WstxInputProperties;
import com.ctc.wstx.msv.W3CMultiSchemaFactory;
/**
* Tests to verify that entities referring to external resources are not
* resolved while reading schema documents; and, conversely, that schema
* composition ({@code xs:include}, {@code xs:import}, RELAX NG
* {@code externalRef}) -- which MSV resolves itself, not via entity
* resolution -- keeps working.
*/
public class TestSchemaExternalEntity
extends BaseValidationTest
{
@TempDir
File tempDir;
/*
///////////////////////////////////////////////////////////////////////
// External entities: must not be resolved
///////////////////////////////////////////////////////////////////////
*/
/**
* External parameter entity: declarations it contains must not be
* pulled into the schema.
*/
@Test
public void testW3CSchemaExternalParameterEntity() throws Exception
{
try {
parseW3CSchema(schemaWithExternalParameterEntity());
fail("Expected failure for schema using an external parameter entity");
} catch (XMLStreamException e) {
// Exact wording is parser-specific; entity name is not
verifyException(e, "injected");
}
}
/**
* Same for the external subset of the schema's own DOCTYPE declaration:
* the external DTD is skipped rather than read, so the declaration it
* carries must not be pulled into the grammar. Because an external subset
* was declared (but left unread), a parser is free either to reject the
* now-undeclared entity or to skip it; either way the injected element name
* must not end up validating. (A schema that references an external DTD but
* uses no entities keeps loading -- see {@link #testW3CSchemaExternalSubsetNoEntity}.)
*/
@Test
public void testW3CSchemaExternalSubset() throws Exception
{
File dtd = writeFile("ext.dtd", "<!ENTITY injected \"viaExternalSubset\">");
String schema =
"<?xml version='1.0'?>\n"
+"<!DOCTYPE xs:schema SYSTEM '"+dtd.toURI()+"'>\n"
+"<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>\n"
+" <xs:element name='&injected;' type='xs:string'/>\n"
+"</xs:schema>";
XMLValidationSchema sch;
try {
sch = parseW3CSchema(schema);
} catch (XMLStreamException e) {
// Acceptable: parser rejected the undeclared entity outright
return;
}
assertFalse("External subset declaration must not be pulled into the grammar",
validates("<viaExternalSubset>x</viaExternalSubset>", sch));
}
/**
* A schema whose DOCTYPE references an external DTD but which uses no
* entities must still load: the external subset is skipped, not treated
* as a fatal error.
*/
@Test
public void testW3CSchemaExternalSubsetNoEntity() throws Exception
{
File dtd = writeFile("ext.dtd", "<!ENTITY injected \"viaExternalSubset\">");
String schema =
"<?xml version='1.0'?>\n"
+"<!DOCTYPE xs:schema SYSTEM '"+dtd.toURI()+"'>\n"
+"<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>\n"
+" <xs:element name='root' type='xs:string'/>\n"
+"</xs:schema>";
XMLValidationSchema sch = parseW3CSchema(schema);
assertTrue("Schema referencing an external DTD but no entities should still load",
validates("<root>x</root>", sch));
}
/**
* Same for schemas read through {@link W3CMultiSchemaFactory}, which
* uses a SAX parser factory of its own. Its failure message carries no
* detail, so the same schema is also loaded with external access enabled,
* to verify the failure is specifically due to the entity not being resolved.
*/
@Test
public void testW3CMultiSchemaExternalParameterEntity() throws Exception
{
File xsd = writeFile("multi.xsd", schemaWithExternalParameterEntity());
try {
new W3CMultiSchemaFactory().createSchema(tempDir.toURI().toString(), multiSources(xsd));
fail("Expected failure for schema using an external parameter entity");
} catch (XMLStreamException e) {
verifyException(e, "Failed to load schemas");
}
XMLValidationSchema sch = new W3CMultiSchemaFactory(true)
.createSchema(tempDir.toURI().toString(), multiSources(xsd));
assertTrue("External parameter entity should be resolved when opted in",
validates("<viaExternalEntity>x</viaExternalEntity>", sch));
}
/**
* External general entity: contents of the referenced file must not end
* up in the grammar.
*/
@Test
public void testRelaxNGExternalEntity() throws Exception
{
final String SECRET = "contentsOfLocalFile";
File secret = writeFile("secret.txt", SECRET);
String schema =
"<?xml version='1.0'?>\n"
+"<!DOCTYPE element [\n"
+" <!ENTITY xxe SYSTEM '"+secret.toURI()+"'>\n"
+"]>\n"
+"<element name='root' xmlns='http://relaxng.org/ns/structure/1.0'>\n"
+" <value>&xxe;</value>\n"
+"</element>";
XMLValidationSchema sch = parseRngSchema(schema);
assertFalse("File contents must not be readable through an external entity",
validates("<root>"+SECRET+"</root>", sch));
}
/**
* ... but a caller can opt back in via
* {@link WstxInputProperties#P_MSV_SCHEMA_EXTERNAL_ACCESS}, which restores
* the legacy behaviour for schemas that legitimately rely on external
* entities.
*/
@Test
public void testRelaxNGExternalEntityWhenAccessEnabled() throws Exception
{
final String SECRET = "contentsOfLocalFile";
File secret = writeFile("secret.txt", SECRET);
String schema =
"<?xml version='1.0'?>\n"
+"<!DOCTYPE element [\n"
+" <!ENTITY xxe SYSTEM '"+secret.toURI()+"'>\n"
+"]>\n"
+"<element name='root' xmlns='http://relaxng.org/ns/structure/1.0'>\n"
+" <value>&xxe;</value>\n"
+"</element>";
XMLValidationSchemaFactory schF =
XMLValidationSchemaFactory.newInstance(XMLValidationSchema.SCHEMA_ID_RELAXNG);
schF.setProperty(WstxInputProperties.P_MSV_SCHEMA_EXTERNAL_ACCESS, Boolean.TRUE);
XMLValidationSchema sch = schF.createSchema(new StringReader(schema));
assertTrue("External entity resolution should be restored when opted in",
validates("<root>"+SECRET+"</root>", sch));
}
/**
* Entities declared in the internal subset are unaffected.
*/
@Test
public void testW3CSchemaInternalEntity() throws Exception
{
String schema =
"<?xml version='1.0'?>\n"
+"<!DOCTYPE xs:schema [ <!ENTITY local 'declaredLocally'> ]>\n"
+"<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>\n"
+" <xs:element name='&local;' type='xs:string'/>\n"
+"</xs:schema>";
XMLValidationSchema sch = parseW3CSchema(schema);
assertTrue("Internal entity should still be expanded",
validates("<declaredLocally>x</declaredLocally>", sch));
}
/*
///////////////////////////////////////////////////////////////////////
// Schema composition: must keep working
///////////////////////////////////////////////////////////////////////
*/
@Test
public void testW3CSchemaInclude() throws Exception
{
writeFile("included.xsd",
"<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>\n"
+" <xs:element name='root' type='xs:string'/>\n"
+"</xs:schema>");
File main = writeFile("including.xsd",
"<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>\n"
+" <xs:include schemaLocation='included.xsd'/>\n"
+"</xs:schema>");
XMLValidationSchema sch = parseSchema(main.toURI().toURL(),
XMLValidationSchema.SCHEMA_ID_W3C_SCHEMA);
assertTrue("'xs:include' should still be resolved",
validates("<root>x</root>", sch));
}
@Test
public void testW3CSchemaImport() throws Exception
{
writeFile("imported.xsd",
"<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema' targetNamespace='urn:sub'>\n"
+" <xs:element name='child' type='xs:string'/>\n"
+"</xs:schema>");
File main = writeFile("importing.xsd",
"<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema' xmlns:s='urn:sub'>\n"
+" <xs:import namespace='urn:sub' schemaLocation='imported.xsd'/>\n"
+" <xs:element name='root'>\n"
+" <xs:complexType><xs:sequence><xs:element ref='s:child'/></xs:sequence></xs:complexType>\n"
+" </xs:element>\n"
+"</xs:schema>");
XMLValidationSchema sch = parseSchema(main.toURI().toURL(),
XMLValidationSchema.SCHEMA_ID_W3C_SCHEMA);
assertTrue("'xs:import' should still be resolved",
validates("<root><child xmlns='urn:sub'>x</child></root>", sch));
}
@Test
public void testRelaxNGExternalRef() throws Exception
{
writeFile("referenced.rng",
"<element name='root' xmlns='http://relaxng.org/ns/structure/1.0'><text/></element>");
File main = writeFile("referring.rng",
"<grammar xmlns='http://relaxng.org/ns/structure/1.0'>\n"
+" <start><externalRef href='referenced.rng'/></start>\n"
+"</grammar>");
XMLValidationSchema sch = parseSchema(main.toURI().toURL(),
XMLValidationSchema.SCHEMA_ID_RELAXNG);
assertTrue("'externalRef' should still be resolved",
validates("<root>x</root>", sch));
}
/*
///////////////////////////////////////////////////////////////////////
// Helper methods
///////////////////////////////////////////////////////////////////////
*/
private String schemaWithExternalParameterEntity() throws Exception
{
File dtd = writeFile("ext.dtd", "<!ENTITY injected \"viaExternalEntity\">");
return "<?xml version='1.0'?>\n"
+"<!DOCTYPE xs:schema [\n"
+" <!ENTITY % ext SYSTEM '"+dtd.toURI()+"'>\n"
+" %ext;\n"
+"]>\n"
+"<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>\n"
+" <xs:element name='&injected;' type='xs:string'/>\n"
+"</xs:schema>";
}
private Map<String,Source> multiSources(File xsd)
{
Map<String,Source> sources = new HashMap<String,Source>();
sources.put("", new StreamSource(xsd.toURI().toString()));
return sources;
}
private boolean validates(String doc, XMLValidationSchema schema) throws XMLStreamException
{
XMLStreamReader2 sr = (XMLStreamReader2) getInputFactory().createXMLStreamReader(new StringReader(doc));
try {
sr.validateAgainst(schema);
while (sr.hasNext()) {
sr.next();
}
return true;
} catch (XMLValidationException e) {
return false;
} finally {
sr.close();
}
}
private File writeFile(String name, String contents) throws Exception
{
File f = new File(tempDir, name);
Writer w = new OutputStreamWriter(new FileOutputStream(f), "UTF-8");
try {
w.write(contents);
} finally {
w.close();
}
return f;
}
}