Home | History | Annotate | Download | only in html
      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 (at) 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 
     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