| // Copyright (c) 2011, Mike Samuel |
| // All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions |
| // are met: |
| // |
| // Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // Redistributions in binary form must reproduce the above copyright |
| // notice, this list of conditions and the following disclaimer in the |
| // documentation and/or other materials provided with the distribution. |
| // Neither the name of the OWASP nor the names of its contributors may |
| // be used to endorse or promote products derived from this software |
| // without specific prior written permission. |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
| // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
| // COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
| // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
| // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
| // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
| // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| // POSSIBILITY OF SUCH DAMAGE. |
| |
| package org.owasp.html; |
| |
| import com.google.common.base.Function; |
| import com.google.common.collect.Lists; |
| |
| import java.io.IOException; |
| import java.io.StringReader; |
| import java.util.List; |
| import java.util.Random; |
| |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.NamedNodeMap; |
| import org.w3c.dom.Node; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.SAXException; |
| |
| import nu.validator.htmlparser.dom.HtmlDocumentBuilder; |
| |
| /** |
| * Throws random policy calls to find evidence against the claim that the |
| * security of the policy is decoupled from that of the parser. |
| * This test is stochastic -- not guaranteed to pass or fail consistently. |
| * If you see a failure, please report it along with the seed from the output. |
| * If you want to repeat a failure, set the system property "junit.seed". |
| * |
| * @author Mike Samuel <mikesamuel@gmail.com> |
| */ |
| public class HtmlPolicyBuilderFuzzerTest extends FuzzyTestCase { |
| |
| final Function<HtmlStreamEventReceiver, HtmlSanitizer.Policy> policyFactory |
| = new HtmlPolicyBuilder() |
| .allowElements("a", "b", "xmp", "pre") |
| .allowAttributes("href").onElements("a") |
| .allowAttributes("title").globally() |
| .allowStandardUrlProtocols() |
| .toFactory(); |
| |
| static final String[] CHUNKS = { |
| "Hello, World!", "<b>", "</b>", |
| "<a onclick='doEvil()' href=javascript:alert(1337)>", "</a>", |
| "<script>", "</script>", "<xmp>", "</xmp>", "javascript:alert(1337)", |
| "<style>", "</style>", "<plaintext>", "<!--", "-->", "<![CDATA[", "]]>", |
| }; |
| |
| static final String[] ELEMENT_NAMES = { |
| "a", "A", |
| "b", "B", |
| "script", "SCRipT", |
| "style", "STYLE", |
| "object", "Object", |
| "noscript", "noScript", |
| "xmp", "XMP", |
| }; |
| |
| static final String[] ATTR_NAMES = { |
| "href", "id", "class", "onclick", "checked", "style", |
| }; |
| |
| public final void testFuzzedOutput() throws IOException, SAXException { |
| boolean passed = false; |
| try { |
| for (int i = 1000; --i >= 0;) { |
| StringBuilder sb = new StringBuilder(); |
| HtmlSanitizer.Policy policy = policyFactory.apply( |
| HtmlStreamRenderer.create(sb, Handler.DO_NOTHING)); |
| policy.openDocument(); |
| List<String> attributes = Lists.newArrayList(); |
| for (int j = 50; --j >= 0;) { |
| int r = rnd.nextInt(3); |
| switch (r) { |
| case 0: |
| attributes.clear(); |
| if (rnd.nextBoolean()) { |
| for (int k = rnd.nextInt(4); --k >= 0;) { |
| attributes.add(pick(rnd, ATTR_NAMES)); |
| attributes.add(pickChunk(rnd)); |
| } |
| } |
| policy.openTag(pick(rnd, ELEMENT_NAMES), attributes); |
| break; |
| case 1: |
| policy.closeTag(pick(rnd, ELEMENT_NAMES)); |
| break; |
| case 2: |
| policy.text(pickChunk(rnd)); |
| break; |
| default: |
| throw new AssertionError( |
| "Randomly chosen number in [0-3) was " + r); |
| } |
| } |
| policy.closeDocument(); |
| |
| String html = sb.toString(); |
| HtmlDocumentBuilder parser = new HtmlDocumentBuilder(); |
| Node node = parser.parseFragment( |
| new InputSource(new StringReader(html)), "body"); |
| checkSafe(node, html); |
| } |
| passed = true; |
| } finally { |
| if (!passed) { |
| System.err.println("Using seed " + seed + "L"); |
| } |
| } |
| } |
| |
| private static void checkSafe(Node node, String html) { |
| switch (node.getNodeType()) { |
| case Node.ELEMENT_NODE: |
| String name = node.getNodeName(); |
| if (!"a".equals(name) && !"b".equals(name) && !"pre".equals(name)) { |
| fail("Illegal element name " + name + " : " + html); |
| } |
| NamedNodeMap attrs = node.getAttributes(); |
| for (int i = 0, n = attrs.getLength(); i < n; ++i) { |
| Attr a = (Attr) attrs.item(i); |
| if ("title".equals(a.getName())) { |
| // ok |
| } else if ("href".equals(a.getName())) { |
| assertEquals(html, "a", name); |
| assertFalse( |
| html, Strings.toLowerCase(a.getValue()).contains("script:")); |
| } |
| } |
| break; |
| } |
| for (Node child = node.getFirstChild(); child != null; |
| child = child.getNextSibling()) { |
| checkSafe(child, html); |
| } |
| } |
| |
| private static String pick(Random rnd, String[] choices) { |
| return choices[rnd.nextInt(choices.length)]; |
| } |
| |
| private static String pickChunk(Random rnd) { |
| String chunk = pick(rnd, CHUNKS); |
| int start = 0; |
| int end = chunk.length(); |
| if (rnd.nextBoolean()) { |
| start = rnd.nextInt(end - 1); |
| } |
| if (end - start < 2 && rnd.nextBoolean()) { |
| end = start + rnd.nextInt(end - start); |
| } |
| return chunk.substring(start, end); |
| } |
| } |