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 java.util.List;
32
33import com.google.common.base.Joiner;
34import com.google.common.collect.ImmutableList;
35import com.google.common.collect.Lists;
36
37import junit.framework.TestCase;
38
39public 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