1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 // FIXME(dominicc): Poor confused check-webkit-style demands Attribute.h here.
6 #include "third_party/blink/renderer/core/dom/attribute.h"
7
8 #include <memory>
9 #include "testing/gtest/include/gtest/gtest.h"
10 #include "third_party/blink/public/common/browser_interface_broker_proxy.h"
11 #include "third_party/blink/renderer/core/clipboard/system_clipboard.h"
12 #include "third_party/blink/renderer/core/dom/qualified_name.h"
13 #include "third_party/blink/renderer/core/editing/editor.h"
14 #include "third_party/blink/renderer/core/editing/frame_selection.h"
15 #include "third_party/blink/renderer/core/editing/selection_template.h"
16 #include "third_party/blink/renderer/core/editing/selection_type.h"
17 #include "third_party/blink/renderer/core/editing/visible_selection.h"
18 #include "third_party/blink/renderer/core/frame/local_frame.h"
19 #include "third_party/blink/renderer/core/html/html_element.h"
20 #include "third_party/blink/renderer/core/html_names.h"
21 #include "third_party/blink/renderer/core/svg/animation/svg_smil_element.h"
22 #include "third_party/blink/renderer/core/svg/properties/svg_property_info.h"
23 #include "third_party/blink/renderer/core/svg/svg_a_element.h"
24 #include "third_party/blink/renderer/core/svg/svg_animate_element.h"
25 #include "third_party/blink/renderer/core/svg/svg_set_element.h"
26 #include "third_party/blink/renderer/core/svg_names.h"
27 #include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
28 #include "third_party/blink/renderer/core/testing/mock_clipboard_host.h"
29 #include "third_party/blink/renderer/core/testing/page_test_base.h"
30 #include "third_party/blink/renderer/core/xlink_names.h"
31 #include "third_party/blink/renderer/platform/geometry/int_size.h"
32 #include "third_party/blink/renderer/platform/heap/heap.h"
33 #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
34 #include "third_party/blink/renderer/platform/weborigin/kurl.h"
35 #include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
36 #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
37 #include "third_party/blink/renderer/platform/wtf/vector.h"
38
39 // Test that SVG content with JavaScript URLs is sanitized by removing
40 // the URLs. This sanitization happens when the content is pasted or
41 // drag-dropped into an editable element.
42 //
43 // There are two vectors for JavaScript URLs in SVG content:
44 //
45 // 1. Attributes, for example xlink:href/href in an <svg:a> element.
46 // 2. Animations which set those attributes, for example
47 // <animate attributeName="xlink:href" values="javascript:...
48 //
49 // The following SVG elements, although related to animation, cannot
50 // set JavaScript URLs:
51 //
52 // - 'animateMotion' does not use attribute name and produces floats
53 // - 'animateTransform' can only animate transform lists
54
55 namespace blink {
56
57 // Pastes |html_to_paste| into the body of |page_holder|'s document, and
58 // verifies the new content of the body is safe and sanitized, and contains
59 // |expected_partial_contents|.
PasteAndVerifySanitization(const char * html_to_paste,const char * expected_partial_contents)60 void PasteAndVerifySanitization(const char* html_to_paste,
61 const char* expected_partial_contents) {
62 auto page_holder = std::make_unique<DummyPageHolder>(IntSize(1, 1));
63 LocalFrame& frame = page_holder.get()->GetFrame();
64
65 // Setup a mock clipboard host.
66 PageTestBase::MockClipboardHostProvider mock_clipboard_host_provider(
67 frame.GetBrowserInterfaceBroker());
68
69 HTMLElement* body = page_holder->GetDocument().body();
70
71 // Make the body editable, and put the caret in it.
72 body->setAttribute(html_names::kContenteditableAttr, "true");
73 body->focus();
74 frame.GetDocument()->UpdateStyleAndLayout(DocumentUpdateReason::kTest);
75 frame.Selection().SetSelectionAndEndTyping(
76 SelectionInDOMTree::Builder().SelectAllChildren(*body).Build());
77 EXPECT_TRUE(frame.Selection().ComputeVisibleSelectionInDOMTree().IsCaret());
78 EXPECT_TRUE(
79 frame.Selection().ComputeVisibleSelectionInDOMTree().IsContentEditable())
80 << "We should be pasting into something editable.";
81
82 frame.GetSystemClipboard()->WriteHTML(html_to_paste, BlankURL(),
83 SystemClipboard::kCannotSmartReplace);
84 frame.GetSystemClipboard()->CommitWrite();
85 // Run all tasks in a message loop to allow asynchronous clipboard writing
86 // to happen before reading from it synchronously.
87 test::RunPendingTasks();
88 EXPECT_TRUE(frame.GetEditor().ExecuteCommand("Paste"));
89
90 // Verify that sanitization during pasting strips JavaScript, but keeps at
91 // least |expected_partial_contents|.
92 String sanitized_content = body->innerHTML();
93 EXPECT_TRUE(sanitized_content.Contains(expected_partial_contents))
94 << "We should have pasted *something*; the document is: "
95 << sanitized_content.Utf8();
96 EXPECT_FALSE(sanitized_content.Contains(":alert()"))
97 << "The JavaScript URL is unsafe and should have been stripped; "
98 "instead: "
99 << sanitized_content.Utf8();
100 }
101
PasteAndVerifyBasicSanitization(const char * unsafe_content)102 void PasteAndVerifyBasicSanitization(const char* unsafe_content) {
103 static const char kMinimalExpectedContents[] = "</a>";
104 PasteAndVerifySanitization(unsafe_content, kMinimalExpectedContents);
105 }
106
107 // Integration tests.
108
TEST(UnsafeSVGAttributeSanitizationTest,pasteAnchor_javaScriptHrefIsStripped)109 TEST(UnsafeSVGAttributeSanitizationTest, pasteAnchor_javaScriptHrefIsStripped) {
110 static const char kUnsafeContent[] =
111 "<svg xmlns='http://www.w3.org/2000/svg' "
112 " width='1cm' height='1cm'>"
113 " <a href='javascript:alert()'></a>"
114 "</svg>";
115 PasteAndVerifyBasicSanitization(kUnsafeContent);
116 }
117
TEST(UnsafeSVGAttributeSanitizationTest,pasteAnchor_javaScriptXlinkHrefIsStripped)118 TEST(UnsafeSVGAttributeSanitizationTest,
119 pasteAnchor_javaScriptXlinkHrefIsStripped) {
120 static const char kUnsafeContent[] =
121 "<svg xmlns='http://www.w3.org/2000/svg' "
122 " xmlns:xlink='http://www.w3.org/1999/xlink'"
123 " width='1cm' height='1cm'>"
124 " <a xlink:href='javascript:alert()'></a>"
125 "</svg>";
126 PasteAndVerifyBasicSanitization(kUnsafeContent);
127 }
128
TEST(UnsafeSVGAttributeSanitizationTest,pasteAnchor_javaScriptHrefIsStripped_caseAndEntityInProtocol)129 TEST(UnsafeSVGAttributeSanitizationTest,
130 pasteAnchor_javaScriptHrefIsStripped_caseAndEntityInProtocol) {
131 static const char kUnsafeContent[] =
132 "<svg xmlns='http://www.w3.org/2000/svg' "
133 " width='1cm' height='1cm'>"
134 " <a href='jAvascriPT:alert()'></a>"
135 "</svg>";
136 PasteAndVerifyBasicSanitization(kUnsafeContent);
137 }
138
TEST(UnsafeSVGAttributeSanitizationTest,pasteAnchor_javaScriptXlinkHrefIsStripped_caseAndEntityInProtocol)139 TEST(UnsafeSVGAttributeSanitizationTest,
140 pasteAnchor_javaScriptXlinkHrefIsStripped_caseAndEntityInProtocol) {
141 static const char kUnsafeContent[] =
142 "<svg xmlns='http://www.w3.org/2000/svg' "
143 " xmlns:xlink='http://www.w3.org/1999/xlink'"
144 " width='1cm' height='1cm'>"
145 " <a xlink:href='jAvascriPT:alert()'></a>"
146 "</svg>";
147 PasteAndVerifyBasicSanitization(kUnsafeContent);
148 }
149
TEST(UnsafeSVGAttributeSanitizationTest,pasteAnchor_javaScriptHrefIsStripped_entityWithoutSemicolonInProtocol)150 TEST(UnsafeSVGAttributeSanitizationTest,
151 pasteAnchor_javaScriptHrefIsStripped_entityWithoutSemicolonInProtocol) {
152 static const char kUnsafeContent[] =
153 "<svg xmlns='http://www.w3.org/2000/svg' "
154 " width='1cm' height='1cm'>"
155 " <a href='javascript:alert()'></a>"
156 "</svg>";
157 PasteAndVerifyBasicSanitization(kUnsafeContent);
158 }
159
TEST(UnsafeSVGAttributeSanitizationTest,pasteAnchor_javaScriptXlinkHrefIsStripped_entityWithoutSemicolonInProtocol)160 TEST(
161 UnsafeSVGAttributeSanitizationTest,
162 pasteAnchor_javaScriptXlinkHrefIsStripped_entityWithoutSemicolonInProtocol) {
163 static const char kUnsafeContent[] =
164 "<svg xmlns='http://www.w3.org/2000/svg' "
165 " xmlns:xlink='http://www.w3.org/1999/xlink'"
166 " width='1cm' height='1cm'>"
167 " <a xlink:href='javascript:alert()'></a>"
168 "</svg>";
169 PasteAndVerifyBasicSanitization(kUnsafeContent);
170 }
171
172 // Other sanitization integration tests are web tests that use
173 // document.execCommand('Copy') to source content that they later
174 // paste. However SVG animation elements are not serialized when
175 // copying, which means we can't test sanitizing these attributes in
176 // web tests: there is nowhere to source the unsafe content from.
TEST(UnsafeSVGAttributeSanitizationTest,pasteAnimatedAnchor_javaScriptHrefIsStripped_caseAndEntityInProtocol)177 TEST(UnsafeSVGAttributeSanitizationTest,
178 pasteAnimatedAnchor_javaScriptHrefIsStripped_caseAndEntityInProtocol) {
179 static const char kUnsafeContent[] =
180 "<svg xmlns='http://www.w3.org/2000/svg' "
181 " width='1cm' height='1cm'>"
182 " <a href='https://www.google.com/'>"
183 " <animate attributeName='href' values='evil;JaVaSCRIpT:alert()'>"
184 " </a>"
185 "</svg>";
186 static const char kExpectedContentAfterSanitization[] =
187 "<a href=\"https://www.goo";
188 PasteAndVerifySanitization(kUnsafeContent, kExpectedContentAfterSanitization);
189 }
190
TEST(UnsafeSVGAttributeSanitizationTest,pasteAnimatedAnchor_javaScriptXlinkHrefIsStripped_caseAndEntityInProtocol)191 TEST(
192 UnsafeSVGAttributeSanitizationTest,
193 pasteAnimatedAnchor_javaScriptXlinkHrefIsStripped_caseAndEntityInProtocol) {
194 static const char kUnsafeContent[] =
195 "<svg xmlns='http://www.w3.org/2000/svg' "
196 " xmlns:xlink='http://www.w3.org/1999/xlink'"
197 " width='1cm' height='1cm'>"
198 " <a xlink:href='https://www.google.com/'>"
199 " <animate xmlns:ng='http://www.w3.org/1999/xlink' "
200 " attributeName='ng:href' "
201 "values='evil;JaVaSCRIpT:alert()'>"
202 " </a>"
203 "</svg>";
204 static const char kExpectedContentAfterSanitization[] =
205 "<a xlink:href=\"https://www.goo";
206 PasteAndVerifySanitization(kUnsafeContent, kExpectedContentAfterSanitization);
207 }
208
209 // Unit tests
210
211 // stripScriptingAttributes inspects animation attributes for
212 // javascript: URLs. This check could be defeated if strings supported
213 // addition. If this test starts failing you must strengthen
214 // Element::stripScriptingAttributes, perhaps to strip all
215 // SVG animation attributes.
TEST(UnsafeSVGAttributeSanitizationTest,stringsShouldNotSupportAddition)216 TEST(UnsafeSVGAttributeSanitizationTest, stringsShouldNotSupportAddition) {
217 auto* document = Document::CreateForTest();
218 auto* target = MakeGarbageCollected<SVGAElement>(*document);
219 auto* element = MakeGarbageCollected<SVGAnimateElement>(*document);
220 element->SetTargetElement(target);
221 element->SetAttributeName(xlink_names::kHrefAttr);
222
223 // Sanity check that xlink:href was identified as a "string" attribute
224 EXPECT_EQ(kAnimatedString, element->GetAnimatedPropertyTypeForTesting());
225
226 EXPECT_FALSE(element->AnimatedPropertyTypeSupportsAddition());
227
228 element->SetAttributeName(svg_names::kHrefAttr);
229
230 // Sanity check that href was identified as a "string" attribute
231 EXPECT_EQ(kAnimatedString, element->GetAnimatedPropertyTypeForTesting());
232
233 EXPECT_FALSE(element->AnimatedPropertyTypeSupportsAddition());
234 }
235
TEST(UnsafeSVGAttributeSanitizationTest,stripScriptingAttributes_animateElement)236 TEST(UnsafeSVGAttributeSanitizationTest,
237 stripScriptingAttributes_animateElement) {
238 Vector<Attribute> attributes;
239 attributes.push_back(Attribute(xlink_names::kHrefAttr, "javascript:alert()"));
240 attributes.push_back(Attribute(svg_names::kHrefAttr, "javascript:alert()"));
241 attributes.push_back(Attribute(svg_names::kFromAttr, "/home"));
242 attributes.push_back(Attribute(svg_names::kToAttr, "javascript:own3d()"));
243
244 auto* document = Document::CreateForTest();
245 auto* element = MakeGarbageCollected<SVGAnimateElement>(*document);
246 element->StripScriptingAttributes(attributes);
247
248 EXPECT_EQ(3ul, attributes.size())
249 << "One of the attributes should have been stripped.";
250 EXPECT_EQ(xlink_names::kHrefAttr, attributes[0].GetName())
251 << "The 'xlink:href' attribute should not have been stripped from "
252 "<animate> because it is not a URL attribute of <animate>.";
253 EXPECT_EQ(svg_names::kHrefAttr, attributes[1].GetName())
254 << "The 'href' attribute should not have been stripped from "
255 "<animate> because it is not a URL attribute of <animate>.";
256 EXPECT_EQ(svg_names::kFromAttr, attributes[2].GetName())
257 << "The 'from' attribute should not have been strippef from <animate> "
258 "because its value is innocuous.";
259 }
260
TEST(UnsafeSVGAttributeSanitizationTest,isJavaScriptURLAttribute_hrefContainingJavascriptURL)261 TEST(UnsafeSVGAttributeSanitizationTest,
262 isJavaScriptURLAttribute_hrefContainingJavascriptURL) {
263 Attribute attribute(svg_names::kHrefAttr, "javascript:alert()");
264 auto* document = Document::CreateForTest();
265 auto* element = MakeGarbageCollected<SVGAElement>(*document);
266 EXPECT_TRUE(element->IsJavaScriptURLAttribute(attribute))
267 << "The 'a' element should identify an 'href' attribute with a "
268 "JavaScript URL value as a JavaScript URL attribute";
269 }
270
TEST(UnsafeSVGAttributeSanitizationTest,isJavaScriptURLAttribute_xlinkHrefContainingJavascriptURL)271 TEST(UnsafeSVGAttributeSanitizationTest,
272 isJavaScriptURLAttribute_xlinkHrefContainingJavascriptURL) {
273 Attribute attribute(xlink_names::kHrefAttr, "javascript:alert()");
274 auto* document = Document::CreateForTest();
275 auto* element = MakeGarbageCollected<SVGAElement>(*document);
276 EXPECT_TRUE(element->IsJavaScriptURLAttribute(attribute))
277 << "The 'a' element should identify an 'xlink:href' attribute with a "
278 "JavaScript URL value as a JavaScript URL attribute";
279 }
280
TEST(UnsafeSVGAttributeSanitizationTest,isJavaScriptURLAttribute_xlinkHrefContainingJavascriptURL_alternatePrefix)281 TEST(
282 UnsafeSVGAttributeSanitizationTest,
283 isJavaScriptURLAttribute_xlinkHrefContainingJavascriptURL_alternatePrefix) {
284 QualifiedName href_alternate_prefix("foo", "href",
285 xlink_names::kNamespaceURI);
286 Attribute evil_attribute(href_alternate_prefix, "javascript:alert()");
287 auto* document = Document::CreateForTest();
288 auto* element = MakeGarbageCollected<SVGAElement>(*document);
289 EXPECT_TRUE(element->IsJavaScriptURLAttribute(evil_attribute))
290 << "The XLink 'href' attribute with a JavaScript URL value should be "
291 "identified as a JavaScript URL attribute, even if the attribute "
292 "doesn't use the typical 'xlink' prefix.";
293 }
294
TEST(UnsafeSVGAttributeSanitizationTest,isSVGAnimationAttributeSettingJavaScriptURL_fromContainingJavaScriptURL)295 TEST(UnsafeSVGAttributeSanitizationTest,
296 isSVGAnimationAttributeSettingJavaScriptURL_fromContainingJavaScriptURL) {
297 Attribute evil_attribute(svg_names::kFromAttr, "javascript:alert()");
298 auto* document = Document::CreateForTest();
299 auto* element = MakeGarbageCollected<SVGAnimateElement>(*document);
300 EXPECT_TRUE(
301 element->IsSVGAnimationAttributeSettingJavaScriptURL(evil_attribute))
302 << "The animate element should identify a 'from' attribute with a "
303 "JavaScript URL value as setting a JavaScript URL.";
304 }
305
TEST(UnsafeSVGAttributeSanitizationTest,isSVGAnimationAttributeSettingJavaScriptURL_toContainingJavaScripURL)306 TEST(UnsafeSVGAttributeSanitizationTest,
307 isSVGAnimationAttributeSettingJavaScriptURL_toContainingJavaScripURL) {
308 Attribute evil_attribute(svg_names::kToAttr, "javascript:window.close()");
309 auto* document = Document::CreateForTest();
310 auto* element = MakeGarbageCollected<SVGSetElement>(*document);
311 EXPECT_TRUE(
312 element->IsSVGAnimationAttributeSettingJavaScriptURL(evil_attribute))
313 << "The set element should identify a 'to' attribute with a JavaScript "
314 "URL value as setting a JavaScript URL.";
315 }
316
TEST(UnsafeSVGAttributeSanitizationTest,isSVGAnimationAttributeSettingJavaScriptURL_valuesContainingJavaScriptURL)317 TEST(
318 UnsafeSVGAttributeSanitizationTest,
319 isSVGAnimationAttributeSettingJavaScriptURL_valuesContainingJavaScriptURL) {
320 Attribute evil_attribute(svg_names::kValuesAttr, "hi!; javascript:confirm()");
321 auto* document = Document::CreateForTest();
322 auto* element = MakeGarbageCollected<SVGAnimateElement>(*document);
323 EXPECT_TRUE(
324 element->IsSVGAnimationAttributeSettingJavaScriptURL(evil_attribute))
325 << "The animate element should identify a 'values' attribute with a "
326 "JavaScript URL value as setting a JavaScript URL.";
327 }
328
TEST(UnsafeSVGAttributeSanitizationTest,isSVGAnimationAttributeSettingJavaScriptURL_innocuousAnimationAttribute)329 TEST(UnsafeSVGAttributeSanitizationTest,
330 isSVGAnimationAttributeSettingJavaScriptURL_innocuousAnimationAttribute) {
331 Attribute fine_attribute(svg_names::kFromAttr, "hello, world!");
332 auto* document = Document::CreateForTest();
333 auto* element = MakeGarbageCollected<SVGSetElement>(*document);
334 EXPECT_FALSE(
335 element->IsSVGAnimationAttributeSettingJavaScriptURL(fine_attribute))
336 << "The animate element should not identify a 'from' attribute with an "
337 "innocuous value as setting a JavaScript URL.";
338 }
339
340 } // namespace blink
341