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 java.util.List;
     32 
     33 import com.google.common.base.Joiner;
     34 import com.google.common.collect.ImmutableList;
     35 import com.google.common.collect.Lists;
     36 
     37 import junit.framework.TestCase;
     38 
     39 public class HtmlStreamRendererTest extends TestCase {
     40 
     41   private final List<String> errors = Lists.newArrayList();
     42   private final StringBuilder rendered = new StringBuilder();
     43   private final HtmlStreamRenderer renderer = HtmlStreamRenderer.create(
     44       rendered, new Handler<String>() {
     45         public void handle(String errorMessage) {
     46           errors.add(errorMessage);
     47         }
     48       });
     49 
     50   @Override
     51   protected void setUp() throws Exception {
     52     super.setUp();
     53     errors.clear();
     54     rendered.setLength(0);
     55   }
     56 
     57   @Override
     58   protected void tearDown() throws Exception {
     59     super.tearDown();
     60     assertTrue(errors.isEmpty());  // Catch any tests that don't check errors.
     61   }
     62 
     63   public final void testEmptyDocument() throws Exception {
     64     assertNormalized("", "");
     65   }
     66 
     67   public final void testElementNamesNormalized() throws Exception {
     68     assertNormalized("<br />", "<br>");
     69     assertNormalized("<br />", "<BR>");
     70     assertNormalized("<br />", "<Br />");
     71     assertNormalized("<br />", "<br\n>");
     72   }
     73 
     74   public final void testAttributeNamesNormalized() throws Exception {
     75     assertNormalized("<input id=\"foo\" />", "<input  id=foo>");
     76     assertNormalized("<input id=\"foo\" />", "<input id=\"foo\">");
     77     assertNormalized("<input id=\"foo\" />", "<input  ID='foo'>");
     78     assertNormalized("<input id=\"foo\" />", "<input\nid='foo'>");
     79     assertNormalized("<input id=\"foo\" />", "<input\nid=foo'>");
     80   }
     81 
     82   public final void testAttributeValuesEscaped() throws Exception {
     83     assertNormalized("<div title=\"a&lt;b\"></div>", "<div title=a<b></div>");
     84   }
     85 
     86   public final void testRcdataEscaped() throws Exception {
     87     assertNormalized(
     88         "<title>I &lt;3 PONIES, OMG!!!</title>",
     89         "<TITLE>I <3 PONIES, OMG!!!</TITLE>");
     90   }
     91 
     92   public final void testCdataNotEscaped() throws Exception {
     93     assertNormalized(
     94         "<script>I <3\n!!!PONIES, OMG</script>",
     95         "<script>I <3\n!!!PONIES, OMG</script>");
     96   }
     97 
     98   public final void testIllegalElementName() throws Exception {
     99     renderer.openDocument();
    100     renderer.openTag(":svg", ImmutableList.<String>of());
    101     renderer.openTag("svg:", ImmutableList.<String>of());
    102     renderer.openTag("-1", ImmutableList.<String>of());
    103     renderer.openTag("svg::svg", ImmutableList.<String>of());
    104     renderer.openTag("a@b", ImmutableList.<String>of());
    105     renderer.closeDocument();
    106 
    107     String output = rendered.toString();
    108     assertFalse(output, output.contains("<"));
    109 
    110     assertEquals(
    111         Joiner.on('\n').join(
    112             "Invalid element name : :svg",
    113             "Invalid element name : svg:",
    114             "Invalid element name : -1",
    115             "Invalid element name : svg::svg",
    116             "Invalid element name : a@b"),
    117         Joiner.on('\n').join(errors));
    118     errors.clear();
    119   }
    120 
    121   public final void testIllegalAttributeName() throws Exception {
    122     renderer.openDocument();
    123     renderer.openTag("div", ImmutableList.of(":svg", "x"));
    124     renderer.openTag("div", ImmutableList.of("svg:", "x"));
    125     renderer.openTag("div", ImmutableList.of("-1", "x"));
    126     renderer.openTag("div", ImmutableList.of("svg::svg", "x"));
    127     renderer.openTag("div", ImmutableList.of("a@b", "x"));
    128     renderer.closeDocument();
    129 
    130     String output = rendered.toString();
    131     assertFalse(output, output.contains("="));
    132 
    133     assertEquals(
    134         Joiner.on('\n').join(
    135             "Invalid attr name : :svg",
    136             "Invalid attr name : svg:",
    137             "Invalid attr name : -1",
    138             "Invalid attr name : svg::svg",
    139             "Invalid attr name : a@b"),
    140         Joiner.on('\n').join(errors));
    141     errors.clear();
    142   }
    143 
    144   public final void testCdataContainsEndTag1() throws Exception {
    145     renderer.openDocument();
    146     renderer.openTag("script", ImmutableList.of("type", "text/javascript"));
    147     renderer.text("document.write('<SCRIPT>alert(42)</SCRIPT>')");
    148     renderer.closeTag("script");
    149     renderer.closeDocument();
    150 
    151     assertEquals(
    152         "<script type=\"text/javascript\"></script>", rendered.toString());
    153     assertEquals(
    154         "Invalid CDATA text content : </SCRIPT>'",
    155         Joiner.on('\n').join(errors));
    156     errors.clear();
    157   }
    158 
    159   public final void testCdataContainsEndTag2() throws Exception {
    160     renderer.openDocument();
    161     renderer.openTag("style", ImmutableList.of("type", "text/css"));
    162     renderer.text("/* </St");
    163     // Split into two text chunks, and insert NULs.
    164     renderer.text("\0yle> */");
    165     renderer.closeTag("style");
    166     renderer.closeDocument();
    167 
    168     assertEquals(
    169         "<style type=\"text/css\"></style>", rendered.toString());
    170     assertEquals(
    171         "Invalid CDATA text content : </Style> *",
    172         Joiner.on('\n').join(errors));
    173     errors.clear();
    174   }
    175 
    176   public final void testRcdataContainsEndTag() throws Exception {
    177     renderer.openDocument();
    178     renderer.openTag("textarea", ImmutableList.<String>of());
    179     renderer.text("<textarea></textarea>");
    180     renderer.closeTag("textarea");
    181     renderer.closeDocument();
    182 
    183     assertEquals(
    184         "<textarea>&lt;textarea&gt;&lt;/textarea&gt;</textarea>",
    185         rendered.toString());
    186   }
    187 
    188   public final void testCdataContainsEndTagInEscapingSpan() throws Exception {
    189     assertNormalized(
    190         "<script><!--document.write('<SCRIPT>alert(42)</SCRIPT>')--></script>",
    191         "<script><!--document.write('<SCRIPT>alert(42)</SCRIPT>')--></script>");
    192   }
    193 
    194   public final void testTagInCdata() throws Exception {
    195     renderer.openDocument();
    196     renderer.openTag("script", ImmutableList.<String>of());
    197     renderer.text("alert('");
    198     renderer.openTag("b", ImmutableList.<String>of());
    199     renderer.text("foo");
    200     renderer.closeTag("b");
    201     renderer.text("')");
    202     renderer.closeTag("script");
    203     renderer.closeDocument();
    204 
    205     assertEquals(
    206         "<script>alert('foo')</script>", rendered.toString());
    207     assertEquals(
    208         Joiner.on('\n').join(
    209             "Tag content cannot appear inside CDATA element : b",
    210             "Tag content cannot appear inside CDATA element : b"),
    211         Joiner.on('\n').join(errors));
    212     errors.clear();
    213   }
    214 
    215   public final void testUnclosedEscapingTextSpan() throws Exception {
    216     renderer.openDocument();
    217     renderer.openTag("script", ImmutableList.<String>of());
    218     renderer.text("<!--alert('</script>')");
    219     renderer.closeTag("script");
    220     renderer.closeDocument();
    221 
    222     assertEquals("<script></script>", rendered.toString());
    223     assertEquals(
    224         "Invalid CDATA text content : <!--alert(",
    225         Joiner.on('\n').join(errors));
    226     errors.clear();
    227   }
    228 
    229   public final void testSupplementaryCodepoints() throws Exception {
    230     renderer.openDocument();
    231     renderer.text("\uD87E\uDC1A");  // Supplementary codepoint U+2F81A
    232     renderer.closeDocument();
    233 
    234     assertEquals("&#x2f81a;", rendered.toString());
    235   }
    236 
    237   // Test that policies that naively allow <xmp>, <listing>, or <plaintext>
    238   // on XHTML don't shoot themselves in the foot.
    239 
    240   public final void testPreSubstitutes1() throws Exception {
    241     renderer.openDocument();
    242     renderer.openTag("Xmp", ImmutableList.<String>of());
    243     renderer.text("<form>Hello, World</form>");
    244     renderer.closeTag("Xmp");
    245     renderer.closeDocument();
    246 
    247     assertEquals("<pre>&lt;form&gt;Hello, World&lt;/form&gt;</pre>",
    248                  rendered.toString());
    249   }
    250 
    251   public final void testPreSubstitutes2() throws Exception {
    252     renderer.openDocument();
    253     renderer.openTag("xmp", ImmutableList.<String>of());
    254     renderer.text("<form>Hello, World</form>");
    255     renderer.closeTag("xmp");
    256     renderer.closeDocument();
    257 
    258     assertEquals("<pre>&lt;form&gt;Hello, World&lt;/form&gt;</pre>",
    259                  rendered.toString());
    260   }
    261 
    262   public final void testPreSubstitutes3() throws Exception {
    263     renderer.openDocument();
    264     renderer.openTag("LISTING", ImmutableList.<String>of());
    265     renderer.text("<form>Hello, World</form>");
    266     renderer.closeTag("LISTING");
    267     renderer.closeDocument();
    268 
    269     assertEquals("<pre>&lt;form&gt;Hello, World&lt;/form&gt;</pre>",
    270                  rendered.toString());
    271   }
    272 
    273   public final void testPreSubstitutes4() throws Exception {
    274     renderer.openDocument();
    275     renderer.openTag("plaintext", ImmutableList.<String>of());
    276     renderer.text("<form>Hello, World</form>");
    277     renderer.closeDocument();
    278 
    279     assertEquals("<pre>&lt;form&gt;Hello, World&lt;/form&gt;",
    280                  rendered.toString());
    281   }
    282 
    283   private void assertNormalized(String golden, String htmlInput)
    284       throws Exception {
    285     assertEquals(golden, normalize(htmlInput));
    286 
    287     // Check that normalization is idempotent.
    288     if (!golden.equals(htmlInput)) {
    289       assertNormalized(golden, golden);
    290     }
    291   }
    292 
    293   private String normalize(String htmlInput) throws Exception {
    294     // Use a permissive sanitizer to generate the events.
    295     HtmlSanitizer.sanitize(htmlInput, new HtmlSanitizer.Policy() {
    296       public void openTag(String elementName, List<String> attrs) {
    297         renderer.openTag(elementName, attrs);
    298       }
    299 
    300       public void closeTag(String elementName) {
    301         renderer.closeTag(elementName);
    302       }
    303 
    304       public void text(String textChunk) {
    305         renderer.text(textChunk);
    306       }
    307 
    308       public void openDocument() {
    309         renderer.openDocument();
    310       }
    311 
    312       public void closeDocument() {
    313         renderer.closeDocument();
    314       }
    315     });
    316 
    317     String result = rendered.toString();
    318     rendered.setLength(0);
    319     return result;
    320   }
    321 }
    322