Skip to content

Commit

Permalink
JCR-3883: Jackrabbit WebDAV bundle susceptible to XXE/XEE attack (CVE…
Browse files Browse the repository at this point in the history
…-2015-1833) (ported to 2.4)

git-svn-id: https://svn.apache.org/repos/asf/jackrabbit/branches/2.4@1680798 13f79535-47bb-0310-9956-ffa450edef68
  • Loading branch information
reschke committed May 21, 2015
1 parent 91754ec commit 17e9f68
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.jackrabbit.webdav.xml;

import java.io.IOException;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;

/**
* Custom {@link DocumentBuilderFactory} extended for use in WebDAV.
*/
public class DavDocumentBuilderFactory {

private static final Logger LOG = LoggerFactory.getLogger(DomUtil.class);

private final DocumentBuilderFactory DEFAULT_FACTORY = createFactory();

private DocumentBuilderFactory BUILDER_FACTORY = DEFAULT_FACTORY;

private DocumentBuilderFactory createFactory() {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setIgnoringComments(true);
factory.setIgnoringElementContentWhitespace(true);
factory.setCoalescing(true);
try {
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch (ParserConfigurationException e) {
LOG.warn("Secure XML processing is not supported", e);
} catch (AbstractMethodError e) {
LOG.warn("Secure XML processing is not supported", e);
}
return factory;
}

public void setFactory(DocumentBuilderFactory documentBuilderFactory) {
LOG.debug("DocumentBuilderFactory changed to: " + documentBuilderFactory);
BUILDER_FACTORY = documentBuilderFactory != null ? documentBuilderFactory : DEFAULT_FACTORY;
}

/**
* An entity resolver that does not allow external entity resolution. See
* RFC 4918, Section 20.6
*/
private static final EntityResolver DEFAULT_ENTITY_RESOLVER = new EntityResolver() {
@Override
public InputSource resolveEntity(String publicId, String systemId) throws IOException {
LOG.debug("Resolution of external entities in XML payload not supported - publicId: " + publicId + ", systemId: "
+ systemId);
throw new IOException("This parser does not support resolution of external entities (publicId: " + publicId
+ ", systemId: " + systemId + ")");
}
};

public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
DocumentBuilder db = BUILDER_FACTORY.newDocumentBuilder();
if (BUILDER_FACTORY == DEFAULT_FACTORY) {
// if this is the default factory: set the default entity resolver as well
db.setEntityResolver(DEFAULT_ENTITY_RESOLVER);
}
db.setErrorHandler(new DefaultHandler());
return db;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@
import org.w3c.dom.Text;
import org.w3c.dom.NamedNodeMap;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
Expand All @@ -56,26 +54,10 @@ public class DomUtil {
private static Logger log = LoggerFactory.getLogger(DomUtil.class);

/**
* Constant for <code>DocumentBuilderFactory</code> which is used
* Constant for <code>DavDocumentBuilderFactory</code> which is used
* to create and parse DOM documents.
*/
private static DocumentBuilderFactory BUILDER_FACTORY = createFactory();

private static DocumentBuilderFactory createFactory() {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setIgnoringComments(true);
factory.setIgnoringElementContentWhitespace(true);
factory.setCoalescing(true);
try {
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch (ParserConfigurationException e) {
log.warn("Secure XML processing is not supported", e);
} catch (AbstractMethodError e) {
log.warn("Secure XML processing is not supported", e);
}
return factory;
}
private static final DavDocumentBuilderFactory BUILDER_FACTORY = new DavDocumentBuilderFactory();

/**
* Support the replacement of {@link #BUILDER_FACTORY}. This is useful
Expand All @@ -88,7 +70,7 @@ private static DocumentBuilderFactory createFactory() {
*/
public static void setBuilderFactory(
DocumentBuilderFactory documentBuilderFactory) {
BUILDER_FACTORY = documentBuilderFactory;
BUILDER_FACTORY.setFactory(documentBuilderFactory);
}

/**
Expand Down Expand Up @@ -119,11 +101,6 @@ public static Document createDocument()
public static Document parseDocument(InputStream stream)
throws ParserConfigurationException, SAXException, IOException {
DocumentBuilder docBuilder = BUILDER_FACTORY.newDocumentBuilder();

// Set an error handler to prevent parsers from printing error messages
// to standard output!
docBuilder.setErrorHandler(new DefaultHandler());

return docBuilder.parse(stream);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.jackrabbit.webdav.xml;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import junit.framework.TestCase;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class ParserTest extends TestCase {

// see <http://en.wikipedia.org/wiki/Billion_laughs#Details>
public void testBillionLaughs() throws UnsupportedEncodingException {

String testBody = "<?xml version=\"1.0\"?>" + "<!DOCTYPE lolz [" + " <!ENTITY lol \"lol\">" + " <!ELEMENT lolz (#PCDATA)>"
+ " <!ENTITY lol1 \"&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;\">"
+ " <!ENTITY lol2 \"&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;\">"
+ " <!ENTITY lol3 \"&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;\">"
+ " <!ENTITY lol4 \"&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;\">"
+ " <!ENTITY lol5 \"&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;\">"
+ " <!ENTITY lol6 \"&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;\">"
+ " <!ENTITY lol7 \"&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;\">"
+ " <!ENTITY lol8 \"&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;\">"
+ " <!ENTITY lol9 \"&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;\">" + "]>" + "<lolz>&lol9;</lolz>";
InputStream is = new ByteArrayInputStream(testBody.getBytes("UTF-8"));

try {
DomUtil.parseDocument(is);
fail("parsing this document should cause an exception");
} catch (Exception expected) {
}
}

public void testExternalEntities() throws IOException {

String dname = "target";
String fname = "test.xml";

File f = new File(dname, fname);
OutputStream os = new FileOutputStream(f);
os.write("testdata".getBytes());
os.close();

String testBody = "<?xml version='1.0'?>\n<!DOCTYPE foo [" + " <!ENTITY test SYSTEM \"file:" + dname + "/" + fname + "\">"
+ "]>\n<foo>&test;</foo>";
InputStream is = new ByteArrayInputStream(testBody.getBytes("UTF-8"));

try {
Document d = DomUtil.parseDocument(is);
Element root = d.getDocumentElement();
String text = DomUtil.getText(root);
fail("parsing this document should cause an exception, but the following external content was included: " + text);
} catch (Exception expected) {
}
}

public void testCustomEntityResolver() throws ParserConfigurationException, SAXException, IOException {

try {
DocumentBuilderFactory dbf = new DocumentBuilderFactory() {

DocumentBuilderFactory def = DocumentBuilderFactory.newInstance();

@Override
public void setFeature(String name, boolean value) throws ParserConfigurationException {
def.setFeature(name, value);
}

@Override
public void setAttribute(String name, Object value) throws IllegalArgumentException {
def.setAttribute(name, value);
}

@Override
public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
DocumentBuilder db = def.newDocumentBuilder();
db.setEntityResolver(new EntityResolver() {
@Override
public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
if ("foo:test".equals(systemId)) {
return new InputSource(new ByteArrayInputStream("foo&amp;bar".getBytes("UTF-8")));
} else {
return null;
}
}
});
return db;
}

@Override
public boolean getFeature(String name) throws ParserConfigurationException {
return def.getFeature(name);
}

@Override
public Object getAttribute(String name) throws IllegalArgumentException {
return def.getAttribute(name);
}
};

DomUtil.setBuilderFactory(dbf);
String testBody = "<?xml version='1.0'?>\n<!DOCTYPE foo [" + " <!ENTITY test SYSTEM \"foo:test\">"
+ "]>\n<foo>&test;</foo>";
InputStream is = new ByteArrayInputStream(testBody.getBytes("UTF-8"));

Document d = DomUtil.parseDocument(is);
Element root = d.getDocumentElement();
String text = DomUtil.getText(root);
assertEquals("custom entity resolver apparently not called", "foo&bar", text);
} finally {
DomUtil.setBuilderFactory(null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public static Test suite() {
TestSuite suite = new TestSuite("org.apache.jackrabbit.webdav.xml tests");

suite.addTestSuite(NamespaceTest.class);
suite.addTestSuite(ParserTest.class);

return suite;
}
Expand Down

0 comments on commit 17e9f68

Please sign in to comment.