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
29package org.owasp.html;
30
31import com.google.common.base.Function;
32import com.google.common.collect.Lists;
33
34import java.io.IOException;
35import java.io.StringReader;
36import java.util.List;
37import java.util.Random;
38
39import org.w3c.dom.Attr;
40import org.w3c.dom.NamedNodeMap;
41import org.w3c.dom.Node;
42import org.xml.sax.InputSource;
43import org.xml.sax.SAXException;
44
45import 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 */
56public 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
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
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
162  private static String pick(Random rnd, String[] choices) {
163    return choices[rnd.nextInt(choices.length)];
164  }
165
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