Skip to content

Commit

Permalink
Fixed #203 - XML bomb vulnerability
Browse files Browse the repository at this point in the history
The current configuration of the SAXParser is vulnerable for an XML bomb
attack. The feature
"http://apache.org/xml/features/disallow-doctype-decl" of the SAXBuilder
must be activated in order to fix the vulnerability.
  • Loading branch information
PatrickGotthard committed Jun 27, 2015
1 parent 46c0061 commit 2bc58a0
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 41 deletions.
33 changes: 20 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@ RSS 0.91 Userland, RSS 0.92, RSS 0.93, RSS 0.94, RSS 1.0, RSS 2.0, Atom 0.3, Ato

More Information: http://rometools.github.io/rome/

## Buildstatus

| Project | Status |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| rome | [![Build Status](http://jenkins.patrick-gotthard.de/buildStatus/icon?job=rome)](http://jenkins.patrick-gotthard.de/job/rome/) |
| rome-certiorem | [![Build Status](http://jenkins.patrick-gotthard.de/buildStatus/icon?job=rome-certiorem)](http://jenkins.patrick-gotthard.de/job/rome-certiorem/) |
| rome-certiorem-webapp | [![Build Status](http://jenkins.patrick-gotthard.de/buildStatus/icon?job=rome-certiorem-webapp)](http://jenkins.patrick-gotthard.de/job/rome-certiorem-webapp/) |
| rome-fetcher | [![Build Status](http://jenkins.patrick-gotthard.de/buildStatus/icon?job=rome-fetcher)](http://jenkins.patrick-gotthard.de/job/rome-fetcher/) |
| rome-modules | [![Build Status](http://jenkins.patrick-gotthard.de/buildStatus/icon?job=rome-modules)](http://jenkins.patrick-gotthard.de/job/rome-modules/) |
| rome-opml | [![Build Status](http://jenkins.patrick-gotthard.de/buildStatus/icon?job=rome-opml)](http://jenkins.patrick-gotthard.de/job/rome-opml/) |
| rome-parent | [![Build Status](http://jenkins.patrick-gotthard.de/buildStatus/icon?job=rome-parent)](http://jenkins.patrick-gotthard.de/job/rome-parent/) |
| rome-propono | [![Build Status](http://jenkins.patrick-gotthard.de/buildStatus/icon?job=rome-propono)](http://jenkins.patrick-gotthard.de/job/rome-propono/) |
| rome-utils | [![Build Status](http://jenkins.patrick-gotthard.de/buildStatus/icon?job=rome-utils)](http://jenkins.patrick-gotthard.de/job/rome-utils/) |
## Changelog

### 1.5.1

- solved an [XML bomb](https://en.wikipedia.org/wiki/Billion_laughs) vulnerability

Important note: due to the security fix ROME now forbids all Doctype declarations by default. This will break compatibility with RSS 0.91 Netscape
because it requires a Doctype declaration. When you experience problems you have to activate the property **allowDoctypes** on the SyndFeedInput object. You
should only use this possibility when the feeds that you process are absolutely trustful.

### 1.5.0

- many (untracked) enhancements
- code cleanup
- renamed packages (was required to be able to push to Maven Central after years again)
- updated sourcecode to Java 1.6

### Prior to 1.5.0

- see [http://rometools.github.io/rome/ROMEReleases](http://rometools.github.io/rome/ROMEReleases)
21 changes: 21 additions & 0 deletions src/main/java/com/rometools/rome/io/SyndFeedInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,27 @@ public void setXmlHealerOn(final boolean heals) {
public boolean getXmlHealerOn() {
return feedInput.getXmlHealerOn();
}

/**
* Indicates whether Doctype declarations are allowed.
*
* @return true when Doctype declarations are allowed, false otherwise
*/
public boolean isAllowDoctypes() {
return feedInput.isAllowDoctypes();
}

/**
* Since ROME 1.5.1 we fixed a security vulnerability by disallowing Doctype declarations by default.
* This change breaks the compatibility with at least RSS 0.91N because it requires a Doctype declaration.
* You are able to allow Doctype declarations again with this property. You should only activate it
* when the feeds that you process are absolutely trustful.
*
* @param allowDoctypes true when Doctype declarations should be allowed again, false otherwise
*/
public void setAllowDoctypes(boolean allowDoctypes) {
feedInput.setAllowDoctypes(allowDoctypes);
}

/**
* Builds SyndFeedImpl from a file.
Expand Down
68 changes: 43 additions & 25 deletions src/main/java/com/rometools/rome/io/WireFeedInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public class WireFeedInput {
private final Locale locale;

private boolean xmlHealerOn;
private boolean allowDoctypes = false;

private static FeedParsers getFeedParsers() {
synchronized (WireFeedInput.class) {
Expand Down Expand Up @@ -163,6 +164,27 @@ public void setXmlHealerOn(final boolean heals) {
public boolean getXmlHealerOn() {
return xmlHealerOn;
}

/**
* Indicates whether Doctype declarations are allowed.
*
* @return true when Doctype declarations are allowed, false otherwise
*/
public boolean isAllowDoctypes() {
return allowDoctypes;
}

/**
* Since ROME 1.5.1 we fixed a security vulnerability by disallowing Doctype declarations by default.
* This change breaks the compatibility with at least RSS 0.91N because it requires a Doctype declaration.
* You are able to allow Doctype declarations again with this property. You should only activate it
* when the feeds that you process are absolutely trustful.
*
* @param allowDoctypes true when Doctype declarations should be allowed again, false otherwise
*/
public void setAllowDoctypes(boolean allowDoctypes) {
this.allowDoctypes = allowDoctypes;
}

/**
* Builds an WireFeed (RSS or Atom) from a file.
Expand Down Expand Up @@ -322,40 +344,36 @@ protected SAXBuilder createSAXBuilder() {
//
// Crimson is one parser which is known not to support these features.
try {

final XMLReader parser = saxBuilder.createParser();
try {
parser.setFeature("http://xml.org/sax/features/external-general-entities", false);
saxBuilder.setFeature("http://xml.org/sax/features/external-general-entities", false);
} catch (final SAXNotRecognizedException e) {
// ignore
} catch (final SAXNotSupportedException e) {
// ignore
}

try {
parser.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
saxBuilder.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
} catch (final SAXNotRecognizedException e) {
// ignore
} catch (final SAXNotSupportedException e) {
// ignore
}

setFeature(saxBuilder, parser, "http://xml.org/sax/features/external-general-entities", false);
setFeature(saxBuilder, parser, "http://xml.org/sax/features/external-parameter-entities", false);
setFeature(saxBuilder, parser, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false);

try {
parser.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
saxBuilder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
} catch (final SAXNotRecognizedException e) {
// ignore
} catch (final SAXNotSupportedException e) {
// ignore
if(!allowDoctypes) {
setFeature(saxBuilder, parser, "http://apache.org/xml/features/disallow-doctype-decl", true);
}

} catch (final JDOMException e) {
throw new IllegalStateException("JDOM could not create a SAX parser");
}

saxBuilder.setExpandEntities(false);

return saxBuilder;

}

private void setFeature(SAXBuilder saxBuilder, XMLReader parser, String feature, boolean value) {
try {
saxBuilder.setFeature(feature, true);
parser.setFeature(feature, true);
} catch (final SAXNotRecognizedException e) {
// ignore
} catch (final SAXNotSupportedException e) {
// ignore
}
}

}
4 changes: 4 additions & 0 deletions src/test/java/com/rometools/rome/unittest/FeedOpsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public abstract class FeedOpsTest extends FeedTest {
protected FeedOpsTest(final String feedType) {
super(feedType + ".xml");
}

protected FeedOpsTest(final String feedType, boolean allowDoctypes) {
super(feedType + ".xml", allowDoctypes);
}

// 1.2a
public void testWireFeedEquals() throws Exception {
Expand Down
9 changes: 9 additions & 0 deletions src/test/java/com/rometools/rome/unittest/FeedTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,22 @@
*
*/
public abstract class FeedTest extends TestCase {

private final String feedFileName;
private final boolean allowDoctypes;
private Document jDomDoc = null;
private WireFeed wireFeed = null;
private SyndFeed syndFeed = null;

private boolean preservingWireFeed = false;

protected FeedTest(final String feedFileName) {
this(feedFileName, false);
}

protected FeedTest(final String feedFileName, boolean allowDoctypes) {
this.feedFileName = feedFileName;
this.allowDoctypes = allowDoctypes;
}

protected String getFeedFileName() {
Expand All @@ -50,12 +57,14 @@ protected Document getJDomDoc() throws Exception {

protected WireFeed getWireFeed() throws Exception {
final WireFeedInput in = new WireFeedInput();
in.setAllowDoctypes(allowDoctypes);
return in.build(getFeedReader());
}

protected SyndFeed getSyndFeed(final boolean preserveWireFeed) throws Exception {
final SyndFeedInput in = new SyndFeedInput();
in.setPreserveWireFeed(preserveWireFeed);
in.setAllowDoctypes(allowDoctypes);
return in.build(getFeedReader());
}

Expand Down
6 changes: 5 additions & 1 deletion src/test/java/com/rometools/rome/unittest/SyndFeedTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ protected SyndFeedTest(final String feedType) {
}

protected SyndFeedTest(final String feedType, final String feedFileName) {
super(feedFileName);
this(feedType, feedFileName, false);
}

protected SyndFeedTest(final String feedType, final String feedFileName, boolean allowDoctypes) {
super(feedFileName, allowDoctypes);
prefix = feedType;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static void main(final String[] args) throws Exception {
}

public TestOpsRSS091N() {
super("rss_0.91N");
super("rss_0.91N", true);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
public class TestSyndFeedRSS091N extends SyndFeedTest {

public TestSyndFeedRSS091N() {
super("rss_0.91N", "rss_0.91N.xml");
super("rss_0.91N", "rss_0.91N.xml", true);
}

protected TestSyndFeedRSS091N(final String type) {
Expand Down
54 changes: 54 additions & 0 deletions src/test/java/com/rometools/rome/unittest/TestXmlBomb.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.rometools.rome.unittest;


import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;

import com.rometools.rome.io.ParsingFeedException;
import com.rometools.rome.io.WireFeedInput;
import junit.framework.TestCase;

public class TestXmlBomb extends TestCase {

public void testXmlBomb() throws Exception {

// https://en.wikipedia.org/wiki/Billion_laughs
// https://msdn.microsoft.com/en-us/magazine/ee335713.aspx
String content = "<?xml version=\"1.0\"?>\n" +
"<!DOCTYPE lolz [\n" +
" <!ENTITY lol \"lol\">\n" +
" <!ELEMENT lolz (#PCDATA)>\n" +
" <!ENTITY lol1 \"&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;\">\n" +
" <!ENTITY lol2 \"&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;\">\n" +
" <!ENTITY lol3 \"&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;\">\n" +
" <!ENTITY lol4 \"&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;\">\n" +
" <!ENTITY lol5 \"&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;\">\n" +
" <!ENTITY lol6 \"&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;\">\n" +
" <!ENTITY lol7 \"&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;\">\n" +
" <!ENTITY lol8 \"&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;\">\n" +
" <!ENTITY lol9 \"&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;\">\n" +
"]>\n" +
"<feed>\n" +
" <title>&lol9;</title>\n" +
" <subtitle>subtitle</subtitle>\n" +
" <entry>\n" +
" <id>id1</id>\n" +
" <title>title1</title>\n" +
" </entry>\n" +
"</feed>";

InputStream is = new ByteArrayInputStream(content.getBytes("UTF-8"));
WireFeedInput feedInput = new WireFeedInput();
Reader reader = new InputStreamReader(is, Charset.forName("UTF-8"));
try {
feedInput.build(reader);
fail("Expected exception");
}
catch (ParsingFeedException ex) {
}
}

}

0 comments on commit 2bc58a0

Please sign in to comment.