Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
import org.apache.roller.weblogger.pojos.Weblog;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.input.SAXBuilder;
import org.apache.roller.weblogger.util.SafeSAXBuilder;

/*
* JPABookmarkManagerImpl.java
Expand Down Expand Up @@ -142,7 +142,7 @@ public void importBookmarks(

try {
// Build JDOC document OPML string
SAXBuilder builder = new SAXBuilder();
SafeSAXBuilder builder = new SafeSAXBuilder();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BookmarksImport shows ex.toString(), so a user whose OPML export has <!DOCTYPE opml> now sees org.apache.roller.weblogger.WebloggerException: org.jdom2.input.JDOMParseException: ... DOCTYPE is disallowed when the feature .... Catching JDOMParseException here and wrapping it with a message like "OPML files with a DOCTYPE are not accepted" would tell them what to do.

StringReader reader = new StringReader( opml );
Document doc = builder.build( reader );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.apache.roller.weblogger.util.SafeSAXBuilder;

import java.io.IOException;
import java.io.InputStream;
Expand All @@ -52,7 +52,7 @@ public ThemeMetadata unmarshall(InputStream instream)

ThemeMetadata theme = new ThemeMetadata();

SAXBuilder builder = new SAXBuilder();
SafeSAXBuilder builder = new SafeSAXBuilder();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A theme.xml with a DOCTYPE is now refused, and ThemeManagerImpl.loadAllThemesFromDisk just logs "Problem processing theme", so the theme vanishes and its weblogs throw ThemeNotFoundException. Shipped themes have no DOCTYPE, so this only hits hand-written ones, but please mention it with the OPML note.

Document doc = builder.build(instream);

// start at root and get theme id, name, description and author
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.apache.roller.weblogger.util.SafeSAXBuilder;


/**
Expand Down Expand Up @@ -57,7 +57,7 @@ public RuntimeConfigDefs unmarshall(InputStream instream)

RuntimeConfigDefs configs = new RuntimeConfigDefs();

SAXBuilder builder = new SAXBuilder();
SafeSAXBuilder builder = new SafeSAXBuilder();
Document doc = builder.build(instream);

Element root = doc.getRootElement();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.apache.roller.weblogger.util.SafeSAXBuilder;

/**
* A helper class for dealing with UI menus.
Expand Down Expand Up @@ -332,7 +332,7 @@ private static ParsedMenu unmarshall(String menuId, InputStream instream)

ParsedMenu config = new ParsedMenu();

SAXBuilder builder = new SAXBuilder();
SafeSAXBuilder builder = new SafeSAXBuilder();
Document doc = builder.build(instream);

Element root = doc.getRootElement();
Expand Down
127 changes: 127 additions & 0 deletions app/src/main/java/org/apache/roller/weblogger/util/SafeSAXBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. 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. For additional
* information regarding copyright in this work, please see the NOTICE
* file in the top level directory of this distribution.
*/

package org.apache.roller.weblogger.util;

import javax.xml.XMLConstants;
import javax.xml.parsers.SAXParserFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.input.sax.XMLReaderJDOMFactory;
import org.xml.sax.XMLReader;

/**
* A {@link SAXBuilder} that treats a document strictly as data.
*
* <p>An XML document can name resources for the parser to go and read: a
* document type declaration can point at an external subset, and entity
* declarations can point at files or URLs. Resolving those makes the parser act
* on behalf of whoever wrote the document, which is only appropriate when the
* document is Roller's own.
*
* <p>Roller parses documents from user input and from its own menu, theme and
* configuration descriptors alike. Rather than track which parser is on which
* side, every retained JDOM parser is built here, and none of them resolve

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite true yet: Trackback.parseTrackbackResponse (Trackback.java:177) still uses a bare new SAXBuilder() on a remote server's response. #163 deletes that file, so either merge this after #163 or switch that call site here too.

* anything. Roller's own descriptors carry no document type declaration, so the
* strict setting costs them nothing.
*
* <p>The settings overlap deliberately. Refusing the declaration outright is
* what does the work; the remaining ones close the same door at the layers
* beneath, so a parser configured elsewhere, or a JAXP implementation with
* different defaults, does not quietly reopen it.
*/
public class SafeSAXBuilder extends SAXBuilder {

/** Xerces feature names, honoured by the JDK's own parser. */
private static final String DISALLOW_DOCTYPE =
"http://apache.org/xml/features/disallow-doctype-decl";
private static final String EXTERNAL_GENERAL_ENTITIES =
"http://xml.org/sax/features/external-general-entities";
private static final String EXTERNAL_PARAMETER_ENTITIES =
"http://xml.org/sax/features/external-parameter-entities";
private static final String LOAD_EXTERNAL_DTD =
"http://apache.org/xml/features/nonvalidating/load-external-dtd";

private static final Log LOG = LogFactory.getLog(SafeSAXBuilder.class);

public SafeSAXBuilder() {
super(new HardenedReaders());

// Secure processing is set explicitly rather than relied on. It is on
// by default in current JDKs, but that default limits resource
// consumption; it does not by itself stop external resolution.
setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);

// A document that declares a doctype is refused. Everything an entity
// could name has to be declared first, so this is the setting the rest
// stand behind.
setFeature(DISALLOW_DOCTYPE, true);

setFeature(EXTERNAL_GENERAL_ENTITIES, false);
setFeature(EXTERNAL_PARAMETER_ENTITIES, false);
setFeature(LOAD_EXTERNAL_DTD, false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: in jdom2 setExpandEntities(false) writes the same feature key as setFeature(EXTERNAL_GENERAL_ENTITIES, false) four lines up; one of the two is a no-op.

setExpandEntities(false);
}

/**
* Supplies the reader, so that the two access properties can be applied
* where a parser that does not recognise them can be tolerated.
*
* <p>They are JAXP properties rather than SAX ones, and Roller ships its
* own Xerces, which rejects them outright at the SAX layer. Setting them
* through the builder would therefore fail every parse. They are still
* worth setting where they are understood, because they deny the protocols
* outright, so they are applied here and a rejection is logged and passed
* over — the features above are what carry the guarantee.
*/
private static final class HardenedReaders implements XMLReaderJDOMFactory {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On this classpath SAXParserFactory.newInstance() resolves to Xerces 2.11 (via nekohtml), which throws SAXNotRecognizedException for both ACCESS_EXTERNAL_DTD and ACCESS_EXTERNAL_SCHEMA, so denyProtocol() always takes the swallowed-exception branch. With DOCTYPE refused there's nothing for those properties to restrict anyway; XMLReaders.NONVALIDATING plus the feature calls above is enough and this inner class can go.


@Override
public XMLReader createXMLReader() throws JDOMException {
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setValidating(false);
XMLReader reader = factory.newSAXParser().getXMLReader();
denyProtocol(reader, XMLConstants.ACCESS_EXTERNAL_DTD);
denyProtocol(reader, XMLConstants.ACCESS_EXTERNAL_SCHEMA);
return reader;
} catch (Exception ex) {
throw new JDOMException("Unable to create an XML reader", ex);
}
}

private void denyProtocol(XMLReader reader, String property) {
try {
reader.setProperty(property, "");
} catch (Exception unsupported) {
LOG.debug("XML reader does not recognise " + property
+ "; the parser features are what constrain resolution", unsupported);
}
}

@Override
public boolean isValidating() {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. 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. For additional information regarding
* copyright in this work, please see the NOTICE file in the top level
* directory of this distribution.
*/
package org.apache.roller.weblogger.business;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.roller.weblogger.TestUtils;
import org.apache.roller.weblogger.pojos.User;
import org.apache.roller.weblogger.pojos.Weblog;
import org.apache.roller.weblogger.pojos.WeblogBookmark;
import org.apache.roller.weblogger.pojos.WeblogBookmarkFolder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Covers the OPML bookmark import's handling of document type declarations:
* documents that carry one are refused, while ordinary OPML still imports.
*/
public class BookmarkImportParsingTest {

private static final Log log = LogFactory.getLog(BookmarkImportParsingTest.class);

private User testUser = null;
private Weblog testWeblog = null;
private final String folderName = "ZZZ_import_parsing_ZZZ";

@BeforeEach
public void setUp() throws Exception {
TestUtils.setupWeblogger();
testUser = TestUtils.setupUser("importParsingTestUser");
testWeblog = TestUtils.setupWeblog("importParsingTestWeblog", testUser);
TestUtils.endSession(true);
}

@AfterEach
public void tearDown() throws Exception {
try {
TestUtils.teardownWeblog(testWeblog.getId());
TestUtils.teardownUser(testUser.getUserName());
TestUtils.endSession(true);
} catch (Exception ex) {
log.error("ERROR in tearDown", ex);
}
}

private BookmarkManager bookmarkManager() {
return WebloggerFactory.getWeblogger().getBookmarkManager();
}

/** @return the bookmarks imported into the test folder, empty if none */
private java.util.List<WeblogBookmark> importedBookmarks() throws Exception {
testWeblog = TestUtils.getManagedWebsite(testWeblog);
WeblogBookmarkFolder folder = bookmarkManager().getFolder(testWeblog, folderName);
if (folder == null) {
return java.util.Collections.emptyList();
}
return folder.retrieveBookmarks();
}

private void tryImport(String opml) {
try {
bookmarkManager().importBookmarks(
TestUtils.getManagedWebsite(testWeblog), folderName, opml);
TestUtils.endSession(true);
} catch (Exception expected) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This catches everything, so the DOCTYPE test passes whenever the import fails for any reason (DB state, a getFolder regression), and the session is left un-ended when importBookmarks throws. Assert on WebloggerException with a JDOMParseException cause, and end the session in a finally.

// A refusal to parse is one acceptable outcome; the assertions in
// each test say what must be true either way.
log.debug("import raised: " + expected);
}
}

/** Ordinary OPML, with no declarations in it, must still import. */
@Test
public void ordinaryOpmlStillImports() throws Exception {
byte[] opml = Files.readAllBytes(
new File("src/test/resources/bookmarks.opml").toPath());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cwd-relative; BookmarkTest and FileContentManagerTest load the same fixture from the classpath.

bookmarkManager().importBookmarks(TestUtils.getManagedWebsite(testWeblog),
folderName, new String(opml, StandardCharsets.UTF_8));
TestUtils.endSession(true);

assertFalse(importedBookmarks().isEmpty(),
"ordinary OPML no longer imports any bookmarks");
}

/** The rejection must not depend on where the DOCTYPE points. */
@Test
public void aDoctypeAloneIsEnoughToBeRefused() throws Exception {
String opml = "<?xml version=\"1.0\"?>"
+ "<!DOCTYPE opml [<!ELEMENT opml ANY>]>"
+ "<opml version=\"1.1\"><head><title>t</title></head><body>"
+ "<outline text=\"harmless\" type=\"link\" url=\"http://example.test/\"/>"
+ "</body></opml>";

tryImport(opml);

assertTrue(importedBookmarks().isEmpty(),
"a document carrying a DOCTYPE was still imported");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. 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. For additional information regarding
* copyright in this work, please see the NOTICE file in the top level
* directory of this distribution.
*/
package org.apache.roller.weblogger.util;

import java.io.StringReader;

import org.jdom2.Document;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

/**
* The parser contract, checked directly rather than through a caller.
*/
public class SafeSAXBuilderTest {

private static final String ORDINARY =
"<?xml version=\"1.0\"?><opml version=\"1.1\"><head><title>t</title>"
+ "</head><body><outline text=\"a\"/></body></opml>";

/** Ordinary XML, carrying no declarations, still parses. */
@Test
public void ordinaryDocumentsStillParse() throws Exception {
Document doc = new SafeSAXBuilder().build(new StringReader(ORDINARY));
assertNotNull(doc.getRootElement());
assertEquals("opml", doc.getRootElement().getName());
}

/** Any document type declaration is refused, whatever it points at. */
@Test
public void anyDoctypeIsRefused() {
String withInternalSubset = "<?xml version=\"1.0\"?>"
+ "<!DOCTYPE opml [<!ELEMENT opml ANY>]>"
+ "<opml version=\"1.1\"><body/></opml>";
assertThrows(Exception.class,
() -> new SafeSAXBuilder().build(new StringReader(withInternalSubset)),
"a document type declaration was accepted");
}
}
Loading