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 org.junit.Test;
34
35import com.google.common.base.Joiner;
36
37import junit.framework.TestCase;
38
39public class HtmlPolicyBuilderTest extends TestCase {
40
41  static final String EXAMPLE = Joiner.on('\n').join(
42      "<h1 id='foo'>Header</h1>",
43      "<p onclick='alert(42)'>Paragraph 1<script>evil()</script></p>",
44      ("<p><a href='java\0script:bad()'>Click</a> <a href='foo.html'>me</a>"
45       + " <a href='http://outside.org/'>out</a></p>"),
46      ("<p><img src=canary.png alt=local-canary>" +
47       "<img src='http://canaries.org/canary.png'></p>"),
48      "<p><b style=font-size:bigger>Fancy</b> with <i><b>soupy</i> tags</b>.",
49      "<p style='color: expression(foo()); text-align: center;",
50      "          /* direction: ltr */; font-weight: bold'>Stylish Para 1</p>",
51      "<p style='color: red; font-weight; expression(foo());",
52      "          direction: rtl; font-weight: bold'>Stylish Para 2</p>",
53      "");
54
55  @Test
56  public static final void testTextFilter() throws Exception {
57    assertEquals(
58        Joiner.on('\n').join(
59            "Header",
60            "Paragraph 1",
61            "Click me out",
62            "",
63            "Fancy with soupy tags.",
64            "Stylish Para 1",
65            "Stylish Para 2",
66            ""),
67        apply(new HtmlPolicyBuilder()));
68  }
69
70  @Test
71  public static final void testCannedFormattingTagFilter() throws Exception {
72    assertEquals(
73        Joiner.on('\n').join(
74            "Header",
75            "Paragraph 1",
76            "Click me out",
77            "",
78            "<b>Fancy</b> with <i><b>soupy</b></i><b> tags</b>.",
79            "Stylish Para 1",
80            "Stylish Para 2",
81            ""),
82        apply(new HtmlPolicyBuilder()
83              .allowCommonInlineFormattingElements()));
84  }
85
86  @Test
87  public static final void testCannedFormattingTagFilterNoItalics()
88      throws Exception {
89    assertEquals(
90        Joiner.on('\n').join(
91            "Header",
92            "Paragraph 1",
93            "Click me out",
94            "",
95            "<b>Fancy</b> with <b>soupy</b><b> tags</b>.",
96            "Stylish Para 1",
97            "Stylish Para 2",
98            ""),
99        apply(new HtmlPolicyBuilder()
100              .allowCommonInlineFormattingElements()
101              .disallowElements("I")));
102  }
103
104  @Test
105  public static final void testSimpleTagFilter() throws Exception {
106    assertEquals(
107        Joiner.on('\n').join(
108            "<h1>Header</h1>",
109            "Paragraph 1",
110            "Click me out",
111            "",
112            "Fancy with <i>soupy</i> tags.",
113            "Stylish Para 1",
114            "Stylish Para 2",
115            ""),
116        apply(new HtmlPolicyBuilder()
117              .allowElements("h1", "i")));
118  }
119
120  @Test
121  public static final void testLinksAllowed() throws Exception {
122    assertEquals(
123        Joiner.on('\n').join(
124            "Header",
125            "Paragraph 1",
126            // We haven't allowed any protocols so only relative URLs are OK.
127            "Click <a href=\"foo.html\">me</a> out",
128            "",
129            "Fancy with soupy tags.",
130            "Stylish Para 1",
131            "Stylish Para 2",
132            ""),
133        apply(new HtmlPolicyBuilder()
134              .allowElements("a")
135              .allowAttributes("href").onElements("a")));
136  }
137
138  @Test
139  public static final void testExternalLinksAllowed() throws Exception {
140    assertEquals(
141        Joiner.on('\n').join(
142            "Header",
143            "Paragraph 1",
144            "Click <a href=\"foo.html\">me</a>"
145            + " <a href=\"http://outside.org/\">out</a>",
146            "",
147            "Fancy with soupy tags.",
148            "Stylish Para 1",
149            "Stylish Para 2",
150            ""),
151        apply(new HtmlPolicyBuilder()
152              .allowElements("a")
153              // Allows http.
154              .allowStandardUrlProtocols()
155              .allowAttributes("href").onElements("a")));
156  }
157
158  @Test
159  public static final void testLinksWithNofollow() throws Exception {
160    assertEquals(
161        Joiner.on('\n').join(
162            "Header",
163            "Paragraph 1",
164            "Click <a href=\"foo.html\" rel=\"nofollow\">me</a> out",
165            "",
166            "Fancy with soupy tags.",
167            "Stylish Para 1",
168            "Stylish Para 2",
169            ""),
170        apply(new HtmlPolicyBuilder()
171              .allowElements("a")
172              // Allows http.
173              .allowAttributes("href").onElements("a")
174              .requireRelNofollowOnLinks()));
175  }
176
177  @Test
178  public static final void testImagesAllowed() throws Exception {
179    assertEquals(
180        Joiner.on('\n').join(
181            "Header",
182            "Paragraph 1",
183            "Click me out",
184            "<img src=\"canary.png\" alt=\"local-canary\" />",
185            // HTTP img not output because only HTTPS allowed.
186            "Fancy with soupy tags.",
187            "Stylish Para 1",
188            "Stylish Para 2",
189            ""),
190        apply(new HtmlPolicyBuilder()
191              .allowElements("img")
192              .allowAttributes("src", "alt").onElements("img")
193              .allowUrlProtocols("https")));
194  }
195
196  @Test
197  public static final void testStyleFiltering() throws Exception {
198    assertEquals(
199        Joiner.on('\n').join(
200            "<h1>Header</h1>",
201            "<p>Paragraph 1</p>",
202            "<p>Click me out</p>",
203            "<p></p>",
204            "<p><b>Fancy</b> with <i><b>soupy</b></i><b> tags</b>.",
205            ("</p><p style=\"text-align:center;font-weight:bold\">"
206             + "Stylish Para 1</p>"),
207            ("<p style=\"color:red;direction:rtl;font-weight:bold\">"
208             + "Stylish Para 2</p>"),
209            ""),
210        apply(new HtmlPolicyBuilder()
211              .allowCommonInlineFormattingElements()
212              .allowCommonBlockElements()
213              .allowStyling()
214              .allowStandardUrlProtocols()));
215  }
216
217  @Test
218  public static final void testElementTransforming() throws Exception {
219    assertEquals(
220        Joiner.on('\n').join(
221            "<div class=\"header-h1\">Header</div>",
222            "<p>Paragraph 1</p>",
223            "<p>Click me out</p>",
224            "<p></p>",
225            "<p>Fancy with soupy tags.",
226            "</p><p>Stylish Para 1</p>",
227            "<p>Stylish Para 2</p>",
228            ""),
229        apply(new HtmlPolicyBuilder()
230              .allowElements("h1", "p", "div")
231              .allowElements(
232                  new ElementPolicy() {
233                    public String apply(
234                        String elementName, List<String> attrs) {
235                      attrs.add("class");
236                      attrs.add("header-" + elementName);
237                      return "div";
238                    }
239                  }, "h1")));
240  }
241
242  @Test
243  public static final void testAllowUrlProtocols() throws Exception {
244    assertEquals(
245        Joiner.on('\n').join(
246            "Header",
247            "Paragraph 1",
248            "Click me out",
249            "<img src=\"canary.png\" alt=\"local-canary\" />"
250            + "<img src=\"http://canaries.org/canary.png\" />",
251            "Fancy with soupy tags.",
252            "Stylish Para 1",
253            "Stylish Para 2",
254            ""),
255            apply(new HtmlPolicyBuilder()
256            .allowElements("img")
257            .allowAttributes("src", "alt").onElements("img")
258            .allowUrlProtocols("http")));
259  }
260
261  @Test
262  public static final void testPossibleFalloutFromIssue5() throws Exception {
263    assertEquals(
264        "Bad",
265        apply(
266            new HtmlPolicyBuilder()
267            .allowElements("a")
268            .allowAttributes("href").onElements("a")
269            .allowUrlProtocols("http"),
270
271            "<a href='javascript:alert(1337)//:http'>Bad</a>"));
272  }
273
274  @Test
275  public static final void testTextInOption() throws Exception {
276    assertEquals(
277        "<select><option>1</option><option>2</option></select>",
278        apply(
279            new HtmlPolicyBuilder()
280            .allowElements("select", "option"),
281
282            "<select>\n  <option>1</option>\n  <option>2</option>\n</select>"));
283  }
284
285  private static String apply(HtmlPolicyBuilder b) throws Exception {
286    return apply(b, EXAMPLE);
287  }
288
289  private static String apply(HtmlPolicyBuilder b, String src)
290      throws Exception {
291    StringBuilder sb = new StringBuilder();
292    HtmlSanitizer.Policy policy = b.build(HtmlStreamRenderer.create(sb,
293        new Handler<String>() {
294          public void handle(String x) { fail(x); }
295        }));
296    HtmlSanitizer.sanitize(src, policy);
297    return sb.toString();
298  }
299}
300