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<b\"></div>", "<div title=a<b></div>"); 84 } 85 86 public final void testRcdataEscaped() throws Exception { 87 assertNormalized( 88 "<title>I <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><textarea></textarea></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("冬", 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><form>Hello, World</form></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><form>Hello, World</form></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><form>Hello, World</form></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><form>Hello, World</form>", 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