Skip to content
Permalink
Browse files
Don't expose raw HTML in pasteboard to the web content
https://bugs.webkit.org/show_bug.cgi?id=178422
Source/WebCore:

<rdar://problem/34567052>

Reviewed by Wenson Hsieh.

This patch enables HTML sanitization added in r223440 when WebKit pastes & concludes edit drag as opposed to
just when dataTransfer.get is used. This is important to avoid leaking privacy sensitive information such as
local file paths and pasting potentially harmful content such as scripts in event handler serialized by
WebKit prior to r223462. In addition, we start using blob URLs in the pasted content instead of retaining
the original URL and overriding the document loader like r222839 for RTFD and r222119 for image files.

To do this, a new superclass FrameWebContentReader of PasteboardWebContentReader and WebContentMarkupReader
is introduced, and helper functions are extracted out of WebContentMarkupReader in WebContentReaderCocoa.mm
to be also used in WebContentReader.

Tests: http/tests/security/clipboard/copy-paste-html-cross-origin-iframe-across-origin.html
       http/tests/security/clipboard/copy-paste-html-cross-origin-iframe-in-same-origin.html
       http/tests/security/clipboard/drag-drop-html-cross-origin-iframe-in-same-origin.html
       PasteWebArchive.SanitizesHTML

* editing/WebContentReader.cpp:
(WebCore::FrameWebContentReader::shouldSanitize const): Moved from WebContentMarkupReader.
* editing/WebContentReader.h:
(WebCore::FrameWebContentReader): Added to share code between WebContentReader and WebContentMarkupReader.
(WebCore::FrameWebContentReader::FrameWebContentReader): Added.
* editing/cocoa/EditorCocoa.mm:
(WebCore::Editor::writeSelectionToPasteboard): Store the content's origin in the pasteboard so that we can
avoid sanitizing the content when pasting into the same document. This is important since converting all URLs
into blob URLs would break editors on the Web which tracks images, etc... in the content using URLs.
(WebCore::Editor::writeSelection): Ditto.
* editing/cocoa/WebContentReaderCocoa.mm:
(WebCore::MarkupAndArchive): Replaced FragmentAndArchive. Now returns the markup string in the archive
instead of the parsed fragment.
(WebCore::extractMarkupAndArchive): Renamed from createFragmentFromWebArchive. Now returns the markup string.
(WebCore::sanitizeMarkupWithArchive): Extracted out of WebContentMarkupReader::readWebArchive to share code
between WebContentReader and WebContentMarkupReader, and added the code to handle subframes recursively.
As inefficient as this code is, we can't delay the conversion of subframes' marksup until later time since
the main frame's markup would contain blob URLs to refer to those subframes.
(WebCore::WebContentReader::readWebArchive): Use sanitizeMarkupWithArchive when shouldSanitize() is true.
Don't add the subresources to the document loader when the content will be loaded into the same origin since
subresouces are mostly likely available in the document anyway.
(WebCore::WebContentMarkupReader::readWebArchive):
* platform/Pasteboard.h:
(WebCore::PasteboardWebContent): Added contentOrigin.
* platform/PasteboardWriterData.h:
(WebCore::PasteboardWriterData): Ditto.
* platform/ios/PasteboardIOS.mm:
(WebCore::Pasteboard::read): Read the origin before branching out to readRespectingUTIFidelities.
* platform/ios/PlatformPasteboardIOS.mm:
(WebCore::PlatformPasteboard::write): Record the content origin into the pasteboard.
* platform/mac/PasteboardMac.mm:
(WebCore::Pasteboard::write): Ditto.
* platform/mac/PasteboardWriter.mm:
(WebCore::createPasteboardWriter): Ditto.

Source/WebKit:


Reviewed by Wenson Hsieh.

Encode & decode the origin string of the copied content written into the system pasteboard.

* Shared/WebCoreArgumentCoders.cpp:
(IPC::ArgumentCoder<PasteboardWebContent>::encode):
(IPC::ArgumentCoder<PasteboardWebContent>::decode):

Tools:


Reviewed by Wenson Hsieh.

Added a test case for sanitizing web archive in the system pasteboard to strip privacy sensitive information
such as local file paths and potentially harmful scripts like event handlers serialized by WebKit prior to r223462.

* TestWebKitAPI/Tests/WebKitCocoa/PasteWebArchive.mm:
(PasteWebArchive.SanitizesHTML):

LayoutTests:


Reviewed by Wenson Hsieh.

Added tests to copy & paste web contents within the same origin as well as cross origin.

* TestExpectations:
* editing/pasteboard/data-transfer-get-data-on-drop-rich-text-expected.txt: Now contains DOCTYPE.
* editing/pasteboard/data-transfer-get-data-on-paste-rich-text-expected.txt: Ditto.
* editing/pasteboard/onpaste-text-html-expected.txt: Rebaselined as now inline styles are stripped.
* editing/pasteboard/onpaste-text-html.html: Strip away the inline style data since they differ on each platform.
* http/tests/misc/copy-resolves-urls-expected.txt:
* http/tests/misc/copy-resolves-urls.html: Now uses blob URL for the pasted image as expected.
* http/tests/security/clipboard/copy-paste-html-cross-origin-iframe-across-origin-expected.txt: Added.
* http/tests/security/clipboard/copy-paste-html-cross-origin-iframe-across-origin.html: Added.
* http/tests/security/clipboard/copy-paste-html-cross-origin-iframe-in-same-origin-expected.txt: Added.
* http/tests/security/clipboard/copy-paste-html-cross-origin-iframe-in-same-origin.html: Added.
* http/tests/security/clipboard/drag-drop-html-cross-origin-iframe-in-same-origin-expected.txt: Added.
* http/tests/security/clipboard/drag-drop-html-cross-origin-iframe-in-same-origin.html: Added.
* http/tests/security/clipboard/resources/content-to-copy.html: Added.
* http/tests/security/clipboard/resources/subdirectory/paste-html.html: Added.
* platform/ios/TestExpectations: Unskip tests that have started passing.
* platform/mac-wk1/TestExpectations: Unskip the drag & drop test which only works in Mac WK1.
* platform/win/TestExpectations: Skip the newly added tests since we don't support custom pasteboard
data on Windows port.


Canonical link: https://commits.webkit.org/194700@main
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@223678 268f45cc-cd09-0410-ab3c-d52691b4dbfc
  • Loading branch information
rniwa committed Oct 19, 2017
1 parent f03d86d commit cf2980ec1d52a190a648d11636920176a4e258b7
Showing 35 changed files with 650 additions and 55 deletions.
@@ -1,3 +1,32 @@
2017-10-18 Ryosuke Niwa <rniwa@webkit.org>

Don't expose raw HTML in pasteboard to the web content
https://bugs.webkit.org/show_bug.cgi?id=178422

Reviewed by Wenson Hsieh.

Added tests to copy & paste web contents within the same origin as well as cross origin.

* TestExpectations:
* editing/pasteboard/data-transfer-get-data-on-drop-rich-text-expected.txt: Now contains DOCTYPE.
* editing/pasteboard/data-transfer-get-data-on-paste-rich-text-expected.txt: Ditto.
* editing/pasteboard/onpaste-text-html-expected.txt: Rebaselined as now inline styles are stripped.
* editing/pasteboard/onpaste-text-html.html: Strip away the inline style data since they differ on each platform.
* http/tests/misc/copy-resolves-urls-expected.txt:
* http/tests/misc/copy-resolves-urls.html: Now uses blob URL for the pasted image as expected.
* http/tests/security/clipboard/copy-paste-html-cross-origin-iframe-across-origin-expected.txt: Added.
* http/tests/security/clipboard/copy-paste-html-cross-origin-iframe-across-origin.html: Added.
* http/tests/security/clipboard/copy-paste-html-cross-origin-iframe-in-same-origin-expected.txt: Added.
* http/tests/security/clipboard/copy-paste-html-cross-origin-iframe-in-same-origin.html: Added.
* http/tests/security/clipboard/drag-drop-html-cross-origin-iframe-in-same-origin-expected.txt: Added.
* http/tests/security/clipboard/drag-drop-html-cross-origin-iframe-in-same-origin.html: Added.
* http/tests/security/clipboard/resources/content-to-copy.html: Added.
* http/tests/security/clipboard/resources/subdirectory/paste-html.html: Added.
* platform/ios/TestExpectations: Unskip tests that have started passing.
* platform/mac-wk1/TestExpectations: Unskip the drag & drop test which only works in Mac WK1.
* platform/win/TestExpectations: Skip the newly added tests since we don't support custom pasteboard
data on Windows port.

2017-10-18 Chris Dumez <cdumez@apple.com>

Implement ServiceWorkerRegistration.scope / updateViaCache
@@ -76,6 +76,7 @@ editing/pasteboard/data-transfer-get-data-on-drop-url.html [ Skip ]
editing/pasteboard/data-transfer-is-unique-for-dragenter-and-dragleave.html [ Skip ]
editing/pasteboard/data-transfer-set-data-sanitize-html-when-dragging-in-null-origin.html [ Skip ]
editing/pasteboard/data-transfer-set-data-sanitize-url-when-dragging-in-null-origin.html [ Skip ]
http/tests/security/clipboard/drag-drop-html-cross-origin-iframe-in-same-origin.html [ Skip ]

editing/pasteboard/drag-end-crash-accessing-item-list.html [ Skip ]
editing/pasteboard/data-transfer-item-list-add-file-on-drag.html [ Skip ]
@@ -5,7 +5,7 @@ Rich text
"text/plain": ""
},
"drop": {
"text/html": "<strong style=\"font-style: normal; font-variant-caps: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; font-family: -apple-system; font-size: 150px; white-space: nowrap; color: purple;\">Rich text</strong>",
"text/html": "<!DOCTYPE html><strong style=\"font-family: -apple-system; font-size: 150px; font-style: normal; font-variant-caps: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: nowrap; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; color: purple;\">Rich text</strong>",
"text/plain": "Rich text"
}
}
@@ -1,7 +1,7 @@
Rich text
{
"paste": {
"text/html": "<span style=\"...\"\">Rich text</span>",
"text/html": "<!DOCTYPE html><span style=\"...\"\">Rich text</span>",
"text/plain": "Rich text"
}
}
@@ -1,5 +1,5 @@
CONSOLE MESSAGE: line 21: text/plain: This test verifies that we can get text/html from the clipboard during an onpaste event.
CONSOLE MESSAGE: line 23: text/html: <span style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; font-size: medium; float: none; display: inline !important;">This test verifies that we can get text/html from the clipboard during an onpaste event.<span class="Apple-converted-space"> </span></span>
CONSOLE MESSAGE: line 23: text/html: <span style="...">This test verifies that we can get text/html from the clipboard during an onpaste event.<span class="Apple-converted-space"> </span></span>
This test verifies that we can get text/html from the clipboard during an onpaste event. This test requires DRT.
Paste content in this div.This test verifies that we can get text/html from the clipboard during an onpaste event. 
PASS
@@ -20,7 +20,7 @@
{
console.log("text/plain: " + ev.clipboardData.getData("text/plain"));
// Remove the font name because it varies depending on the platform.
console.log("text/html: " + removeFontName(ev.clipboardData.getData("text/html")));
console.log("text/html: " + ev.clipboardData.getData("text/html").replace(/style="[^"]+"/, 'style="..."'));
if (ev.clipboardData.getData("text/html") != undefined)
document.getElementById("results").innerHTML = "PASS";
}
@@ -1,4 +1,4 @@
CONSOLE MESSAGE: line 21: text/plain: This test verifies that we can get text/html from the drag object during an ondrop event.
CONSOLE MESSAGE: line 23: text/html: <span style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; font-size: medium; float: none; display: inline !important;">This test verifies that we can get text/html from the drag object during an ondrop event.<span class="Apple-converted-space"> </span></span>
CONSOLE MESSAGE: line 23: text/html: <span style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-size: medium; font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; display: inline !important; float: none;">This test verifies that we can get text/html from the drag object during an ondrop event.<span class="Apple-converted-space"> </span></span>
This test verifies that we can get text/html from the drag object during an ondrop event. This test requires DRT.
PASS
@@ -2,4 +2,4 @@ This tests to make sure that copying/pasting HTML results in URLs being full pat

link
link
<a href="http://127.0.0.1:8000/relative/path/foo.html">link</a><img src="http://127.0.0.1:8000/misc/resources/compass.jpg">
<a href="http://127.0.0.1:8000/relative/path/foo.html">link</a><img src="blob://localhost:8080/...">
@@ -42,13 +42,15 @@
{
var pasteHere = document.getElementById("pastehere");
var results = document.getElementById("results");
results.appendChild(document.createTextNode(pasteHere.innerHTML));
results.appendChild(document.createTextNode(pasteHere.innerHTML.replace(/blob\:http\:\/\/localhost\:8080\/[a-z0-9\-]+/, 'blob://localhost:8080/...')));
if (window.testRunner)
testRunner.notifyDone();
}

if (document.location.search == "?paste")
doPaste();
else
test();
window.onload = () => {
if (document.location.search == "?paste")
doPaste();
else
test();
}
</script>
@@ -0,0 +1,24 @@
This tests copying and pasting HTML by the default action. WebKit should sanitize the HTML across origin.

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


html in DataTransfer
PASS html.includes("hello") is true
PASS fragment = (new DOMParser).parseFromString(html, "text/html"); img = fragment.querySelector("img"); !!img is true
PASS new URL(img.src).protocol is "blob:"
PASS new URL(fragment.querySelector(".same-origin-frame").src).protocol is "blob:"
PASS new URL(fragment.querySelector(".cross-origin-frame").src).protocol is "blob:"
PASS frames.length is 2
PASS new URL(frames[0].src).protocol is "blob:"
PASS frames[0].canAccessContentDocument is true
PASS frames[0].hasImage is true
PASS frames[0].imageWidth is 80
PASS new URL(frames[1].src).protocol is "blob:"
PASS frames[1].canAccessContentDocument is true
PASS frames[1].hasImage is true
PASS frames[1].imageWidth is 80
PASS successfullyParsed is true

TEST COMPLETE

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<body>
<script src="/resources/js-test-pre.js"></script>
<p id="description"></p>
<div id="console"></div>
<div id="container">
<button onclick="runTest()">Copy</button>
<div><br></div>
<div id="source" contenteditable>
hello, <meta content="some secret"><!-- secret -->
<img onclick="dangerousCode()" src="resources/apple.gif"><br>
<iframe class="same-origin-frame" src="resources/content-to-copy.html" width=80 height=80></iframe>
<iframe class="cross-origin-frame" src="http://localhost:8080/security/clipboard/resources/content-to-copy.html" width="100" height="100"></iframe>
</div>
<iframe id="destinationFrame" src="http://localhost:8000/security/clipboard/resources/subdirectory/paste-html.html"></iframe>
</div>
<script>

description('This tests copying and pasting HTML by the default action. WebKit should sanitize the HTML across origin.');
jsTestIsAsync = true;

if (window.internals)
internals.settings.setCustomPasteboardDataEnabled(true);

function runTest() {
document.getElementById('source').focus();
document.execCommand('selectAll');
document.execCommand('copy');
getSelection().removeAllRanges();
setTimeout(() => {
destinationFrame.postMessage({type: 'paste'}, '*');
}, 0);
}

window.onmessage = function (event) {
if (event.data.type == 'pasted') {
html = event.data.html;
debug('html in DataTransfer');
shouldBeTrue('html.includes("hello")');
shouldBeTrue('fragment = (new DOMParser).parseFromString(html, "text/html"); img = fragment.querySelector("img"); !!img');
shouldBeEqualToString('new URL(img.src).protocol', 'blob:');
shouldBeEqualToString('new URL(fragment.querySelector(".same-origin-frame").src).protocol', 'blob:');
shouldBeEqualToString('new URL(fragment.querySelector(".cross-origin-frame").src).protocol', 'blob:');
} else if (event.data.type == 'checkedState') {
frames = event.data.frames;
shouldBe('frames.length', '2');
shouldBeEqualToString('new URL(frames[0].src).protocol', 'blob:');
shouldBeTrue('frames[0].canAccessContentDocument');
shouldBeTrue('frames[0].hasImage');
shouldBe('frames[0].imageWidth', '80');
shouldBeEqualToString('new URL(frames[1].src).protocol', 'blob:');
shouldBeTrue('frames[1].canAccessContentDocument');
shouldBeTrue('frames[1].hasImage');
shouldBe('frames[1].imageWidth', '80');
if (window.testRunner)
container.remove();
finishJSTest();
}
}

if (window.testRunner)
window.onload = runTest;

setTimeout(finishJSTest, 3000);

</script>
<script src="/resources/js-test-post.js"></script>
</body>
</html>
@@ -0,0 +1,24 @@
CONSOLE MESSAGE: line 233: Blocked a frame with origin "http://127.0.0.1:8000" from accessing a frame with origin "http://localhost:8000". Protocols, domains, and ports must match.
CONSOLE MESSAGE: line 233: Blocked a frame with origin "http://127.0.0.1:8000" from accessing a frame with origin "http://localhost:8000". Protocols, domains, and ports must match.
This tests copying and pasting HTML by the default action. WebKit should not sanitize the HTML in the same document.

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


PASS html = event.clipboardData.getData("text/html"); html.includes("hello") is true
PASS destination.innerHTML = html; img = destination.querySelector("img"); !!img is true
PASS new URL(img.src).protocol is "http:"
PASS html.includes("http://localhost:8000/security/clipboard/resources/content-to-copy.html") is true
PASS html.includes("secret") is false
destination.innerHTML = ""
PASS destination.textContent.includes("hello") is true
PASS destination.innerHTML.includes("secret") is false
PASS destination.innerHTML.includes("dangerousCode") is false
PASS destination.querySelector("img"); !!img is true
PASS new URL(img.src).protocol is "http:"
PASS source.querySelector("iframe").contentDocument is null
PASS destination.querySelector("iframe").contentDocument is null
PASS successfullyParsed is true

TEST COMPLETE

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html>
<body>
<script src="/resources/js-test-pre.js"></script>
<p id="description"></p>
<div id="console"></div>
<div id="container">
<button onclick="runTest()">Copy</button>
<div><br></div>
<div id="source" contenteditable>
hello, <meta content="some secret"><!-- secret -->
<img onclick="dangerousCode()" src="resources/apple.gif"><br>
<iframe src="http://localhost:8000/security/clipboard/resources/content-to-copy.html"></iframe>
</div>
<div id="destination" onpaste="doPaste(event)" contenteditable>Paste here</div>
</div>
<script>

description('This tests copying and pasting HTML by the default action. WebKit should not sanitize the HTML in the same document.');
jsTestIsAsync = true;

if (window.internals)
internals.settings.setCustomPasteboardDataEnabled(true);

function runTest() {
document.getElementById('source').focus();
document.execCommand('selectAll');
document.execCommand('copy');
setTimeout(() => {
document.getElementById('destination').focus();
document.execCommand('selectAll');
if (window.testRunner)
document.execCommand('paste');
}, 0);
}

function doPaste(event) {
shouldBeTrue('html = event.clipboardData.getData("text/html"); html.includes("hello")');
shouldBeTrue('destination.innerHTML = html; img = destination.querySelector("img"); !!img');
shouldBeEqualToString('new URL(img.src).protocol', 'http:');
shouldBeTrue('html.includes("http://localhost:8000/security/clipboard/resources/content-to-copy.html")');
shouldBeFalse('html.includes("secret")');
evalAndLog('destination.innerHTML = ""');

const observer = new MutationObserver((recordList) => {
for (const record of recordList) {
for (const node of record.addedNodes) {
if (node.nodeValue === null)
continue;
if (node.nodeValue.includes('secret'))
testFailed(`Saw secret in a node ${node}`);
if (node.nodeValue.includes('dangerousCode'))
testFailed(`Saw dangerous code in a node ${node}`);
}
}
});
observer.observe(destination, {childList: true, subtree: true});

window.onmessage = checkFrameAccess;
}

function checkFrameAccess() {
shouldBeTrue('destination.textContent.includes("hello")');
shouldBeFalse('destination.innerHTML.includes("secret")');
shouldBeFalse('destination.innerHTML.includes("dangerousCode")');
shouldBeTrue('destination.querySelector("img"); !!img');
shouldBeEqualToString('new URL(img.src).protocol', 'http:');
shouldBeNull('source.querySelector("iframe").contentDocument');
shouldBeNull('destination.querySelector("iframe").contentDocument');
container.remove();
finishJSTest();
}

if (window.testRunner)
window.onload = runTest;

var successfullyParsed = true;

</script>
<script src="/resources/js-test-post.js"></script>
</body>
</html>
@@ -0,0 +1,24 @@
CONSOLE MESSAGE: line 233: Blocked a frame with origin "http://127.0.0.1:8000" from accessing a frame with origin "http://localhost:8000". Protocols, domains, and ports must match.
This tests draggin and dropping HTML by the default action. WebKit should not sanitize the HTML in the same document.
To manually test, drag & drop the content in the block above to the blue box on the right.

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


PASS html = event.dataTransfer.getData("text/html"); html.includes("Drag this,") is true
PASS destination.innerHTML = html; img = destination.querySelector("img"); !!img is true
PASS new URL(img.src).protocol is "http:"
PASS html.includes("http://localhost:8000/security/clipboard/resources/content-to-copy.html") is true
PASS html.includes("secret") is false
destination.innerHTML = ""
PASS source.innerHTML is ""
PASS destination.textContent.includes("Drag this,") is true
PASS destination.innerHTML.includes("secret") is false
PASS destination.innerHTML.includes("dangerousCode") is false
PASS destination.querySelector("img"); !!img is true
PASS new URL(img.src).protocol is "http:"
PASS destination.querySelector("iframe").contentDocument is null
PASS successfullyParsed is true

TEST COMPLETE

0 comments on commit cf2980e

Please sign in to comment.