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='j&#x41;vascriPT: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='j&#x41;vascriPT: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='jav&#x61script: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='jav&#x61script: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;J&#x61VaSCRIpT: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;J&#x61VaSCRIpT: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