1 // Copyright (c) 2011, Mike Samuel 2 // All rights reserved. 3 // 4 // Redistribution and use in source and binary forms, with or without 5 // modification, are permitted provided that the following conditions 6 // are met: 7 // 8 // Redistributions of source code must retain the above copyright 9 // notice, this list of conditions and the following disclaimer. 10 // Redistributions in binary form must reproduce the above copyright 11 // notice, this list of conditions and the following disclaimer in the 12 // documentation and/or other materials provided with the distribution. 13 // Neither the name of the OWASP nor the names of its contributors may 14 // be used to endorse or promote products derived from this software 15 // without specific prior written permission. 16 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 19 // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 20 // COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21 // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22 // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 25 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 26 // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 // POSSIBILITY OF SUCH DAMAGE. 28 29 package org.owasp.html; 30 31 import com.google.common.base.Function; 32 import com.google.common.collect.Lists; 33 34 import java.io.IOException; 35 import java.io.StringReader; 36 import java.util.List; 37 import java.util.Random; 38 39 import org.w3c.dom.Attr; 40 import org.w3c.dom.NamedNodeMap; 41 import org.w3c.dom.Node; 42 import org.xml.sax.InputSource; 43 import org.xml.sax.SAXException; 44 45 import nu.validator.htmlparser.dom.HtmlDocumentBuilder; 46 47 /** 48 * Throws random policy calls to find evidence against the claim that the 49 * security of the policy is decoupled from that of the parser. 50 * This test is stochastic -- not guaranteed to pass or fail consistently. 51 * If you see a failure, please report it along with the seed from the output. 52 * If you want to repeat a failure, set the system property "junit.seed". 53 * 54 * @author Mike Samuel <mikesamuel@gmail.com> 55 */ 56 public class HtmlPolicyBuilderFuzzerTest extends FuzzyTestCase { 57 58 final Function<HtmlStreamEventReceiver, HtmlSanitizer.Policy> policyFactory 59 = new HtmlPolicyBuilder() 60 .allowElements("a", "b", "xmp", "pre") 61 .allowAttributes("href").onElements("a") 62 .allowAttributes("title").globally() 63 .allowStandardUrlProtocols() 64 .toFactory(); 65 66 static final String[] CHUNKS = { 67 "Hello, World!", "<b>", "</b>", 68 "<a onclick='doEvil()' href=javascript:alert(1337)>", "</a>", 69 "<script>", "</script>", "<xmp>", "</xmp>", "javascript:alert(1337)", 70 "<style>", "</style>", "<plaintext>", "<!--", "-->", "<![CDATA[", "]]>", 71 }; 72 73 static final String[] ELEMENT_NAMES = { 74 "a", "A", 75 "b", "B", 76 "script", "SCRipT", 77 "style", "STYLE", 78 "object", "Object", 79 "noscript", "noScript", 80 "xmp", "XMP", 81 }; 82 83 static final String[] ATTR_NAMES = { 84 "href", "id", "class", "onclick", "checked", "style", 85 }; 86 testFuzzedOutput()87 public final void testFuzzedOutput() throws IOException, SAXException { 88 boolean passed = false; 89 try { 90 for (int i = 1000; --i >= 0;) { 91 StringBuilder sb = new StringBuilder(); 92 HtmlSanitizer.Policy policy = policyFactory.apply( 93 HtmlStreamRenderer.create(sb, Handler.DO_NOTHING)); 94 policy.openDocument(); 95 List<String> attributes = Lists.newArrayList(); 96 for (int j = 50; --j >= 0;) { 97 int r = rnd.nextInt(3); 98 switch (r) { 99 case 0: 100 attributes.clear(); 101 if (rnd.nextBoolean()) { 102 for (int k = rnd.nextInt(4); --k >= 0;) { 103 attributes.add(pick(rnd, ATTR_NAMES)); 104 attributes.add(pickChunk(rnd)); 105 } 106 } 107 policy.openTag(pick(rnd, ELEMENT_NAMES), attributes); 108 break; 109 case 1: 110 policy.closeTag(pick(rnd, ELEMENT_NAMES)); 111 break; 112 case 2: 113 policy.text(pickChunk(rnd)); 114 break; 115 default: 116 throw new AssertionError( 117 "Randomly chosen number in [0-3) was " + r); 118 } 119 } 120 policy.closeDocument(); 121 122 String html = sb.toString(); 123 HtmlDocumentBuilder parser = new HtmlDocumentBuilder(); 124 Node node = parser.parseFragment( 125 new InputSource(new StringReader(html)), "body"); 126 checkSafe(node, html); 127 } 128 passed = true; 129 } finally { 130 if (!passed) { 131 System.err.println("Using seed " + seed + "L"); 132 } 133 } 134 } 135 checkSafe(Node node, String html)136 private static void checkSafe(Node node, String html) { 137 switch (node.getNodeType()) { 138 case Node.ELEMENT_NODE: 139 String name = node.getNodeName(); 140 if (!"a".equals(name) && !"b".equals(name) && !"pre".equals(name)) { 141 fail("Illegal element name " + name + " : " + html); 142 } 143 NamedNodeMap attrs = node.getAttributes(); 144 for (int i = 0, n = attrs.getLength(); i < n; ++i) { 145 Attr a = (Attr) attrs.item(i); 146 if ("title".equals(a.getName())) { 147 // ok 148 } else if ("href".equals(a.getName())) { 149 assertEquals(html, "a", name); 150 assertFalse( 151 html, Strings.toLowerCase(a.getValue()).contains("script:")); 152 } 153 } 154 break; 155 } 156 for (Node child = node.getFirstChild(); child != null; 157 child = child.getNextSibling()) { 158 checkSafe(child, html); 159 } 160 } 161 pick(Random rnd, String[] choices)162 private static String pick(Random rnd, String[] choices) { 163 return choices[rnd.nextInt(choices.length)]; 164 } 165 pickChunk(Random rnd)166 private static String pickChunk(Random rnd) { 167 String chunk = pick(rnd, CHUNKS); 168 int start = 0; 169 int end = chunk.length(); 170 if (rnd.nextBoolean()) { 171 start = rnd.nextInt(end - 1); 172 } 173 if (end - start < 2 && rnd.nextBoolean()) { 174 end = start + rnd.nextInt(end - start); 175 } 176 return chunk.substring(start, end); 177 } 178 } 179