Title: [253486] trunk
Revision
253486
Author
[email protected]
Date
2019-12-13 10:39:40 -0800 (Fri, 13 Dec 2019)

Log Message

[Clipboard API] Sanitize HTML and image data written using clipboard.write
https://bugs.webkit.org/show_bug.cgi?id=205188
<rdar://problem/57612968>

Reviewed by Darin Adler.

Source/WebCore:

Sanitizes HTML ("text/html") and image ("image/png") content when writing to the platform pasteboard using the
clipboard API. See below for more details.

Tests: editing/async-clipboard/sanitize-when-reading-markup.html
       editing/async-clipboard/sanitize-when-writing-image.html
       ClipboardTests.WriteSanitizedMarkup

* Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp:
(WebCore::ClipboardItemBindingsDataSource::ClipboardItemTypeLoader::sanitizeDataIfNeeded):

Add a new helper method to sanitize `m_data` after loading finishes, but before invoking the completion handler
to notify the data source about the clipboard data. Currently, we support writing "text/plain", "text/uri-list",
"text/html" and "image/png"; we sanitize HTML by stripping away hidden content such as comments, script, and
event listeners, and sanitize image data by painting it into a new graphics context and re-encoding the rendered
contents as an image.

(WebCore::ClipboardItemBindingsDataSource::ClipboardItemTypeLoader::invokeCompletionHandler):
* Modules/async-clipboard/ClipboardItemBindingsDataSource.h:
* platform/PlatformPasteboard.h:
* platform/cocoa/PasteboardCocoa.mm:
(WebCore::cocoaTypeToImageType):
* platform/ios/PlatformPasteboardIOS.mm:
(WebCore::PlatformPasteboard::platformPasteboardTypeForSafeTypeForDOMToReadAndWrite):

Adjust these helpers to map "image/png" to the platform PNG type on iOS ("public.png"). The extra
IncludeImageTypes argument was added to avoid considering "image/png" a web-safe type for writing and reading
when exercising DataTransfer APIs, which currently don't support reading the "image/png" MIME type. In the
future, we should consider adding support for image sanitization when using DataTransfer.setData or
DataTransferItemList.add, and then remove this flag.

(WebCore::createItemProviderRegistrationList):
* platform/mac/LegacyNSPasteboardTypes.h:

Add an entry for legacyPNGPasteboardType on macOS, and replace one use of it in PasteboardCocoa.mm.

(WebCore::legacyPNGPasteboardType):
* platform/mac/PlatformPasteboardMac.mm:
(WebCore::PlatformPasteboard::write):
(WebCore::PlatformPasteboard::platformPasteboardTypeForSafeTypeForDOMToReadAndWrite):

Likewise, map "image/png" to legacyPNGPasteboardType() on macOS, which was added above.

(WebCore::createPasteboardItem):

Tools:

Adds an API test to verify that the markup written to the platform pasteboard on macOS and iOS is sanitized, and
does not contain hidden content, such as script elements.

* TestWebKitAPI/Tests/WebKitCocoa/ClipboardTests.mm:
(-[TestWKWebView writeString:toClipboardWithType:]):
(readMarkupFromPasteboard):
* TestWebKitAPI/Tests/WebKitCocoa/clipboard.html:

LayoutTests:

* editing/async-clipboard/sanitize-when-reading-markup-expected.txt: Added.
* editing/async-clipboard/sanitize-when-reading-markup.html: Added.

Add a test to verify that markup is sanitized when copying and pasting across different security origins.

* editing/async-clipboard/sanitize-when-writing-image-expected.txt: Added.
* editing/async-clipboard/sanitize-when-writing-image.html: Added.

Add a test to verify that "image/png" data is sanitized, and one or more written image data that cannot be
decoded results in the promise being rejected.

* platform/mac-wk1/TestExpectations:
* platform/win/TestExpectations:

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (253485 => 253486)


--- trunk/LayoutTests/ChangeLog	2019-12-13 18:35:06 UTC (rev 253485)
+++ trunk/LayoutTests/ChangeLog	2019-12-13 18:39:40 UTC (rev 253486)
@@ -1,3 +1,25 @@
+2019-12-13  Wenson Hsieh  <[email protected]>
+
+        [Clipboard API] Sanitize HTML and image data written using clipboard.write
+        https://bugs.webkit.org/show_bug.cgi?id=205188
+        <rdar://problem/57612968>
+
+        Reviewed by Darin Adler.
+
+        * editing/async-clipboard/sanitize-when-reading-markup-expected.txt: Added.
+        * editing/async-clipboard/sanitize-when-reading-markup.html: Added.
+
+        Add a test to verify that markup is sanitized when copying and pasting across different security origins.
+
+        * editing/async-clipboard/sanitize-when-writing-image-expected.txt: Added.
+        * editing/async-clipboard/sanitize-when-writing-image.html: Added.
+
+        Add a test to verify that "image/png" data is sanitized, and one or more written image data that cannot be
+        decoded results in the promise being rejected.
+
+        * platform/mac-wk1/TestExpectations:
+        * platform/win/TestExpectations:
+
 2019-12-13  Chris Dumez  <[email protected]>
 
         Behavior of [[GetOwnProperty]] for cross-origin windows is not spec-compliant

Added: trunk/LayoutTests/editing/async-clipboard/sanitize-when-reading-markup-expected.txt (0 => 253486)


--- trunk/LayoutTests/editing/async-clipboard/sanitize-when-reading-markup-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/async-clipboard/sanitize-when-reading-markup-expected.txt	2019-12-13 18:39:40 UTC (rev 253486)
@@ -0,0 +1,22 @@
+This test verifies that navigator.clipboard.writeText sanitizes 'text/html'. To manually run the test, click the button in the subframe below to copy HTML, and then click the paste button.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS Wrote markup to the clipboard.
+PASS finishedCopying became true
+PASS Read items from the clipboard.
+PASS items.length is 2
+PASS items[0].types is ['text/html']
+PASS fragment1.documentElement.textContent is "Hello world 1"
+PASS fragment1.querySelector('p') is non-null.
+PASS fragment1.querySelector('script') is null
+PASS fragment1.querySelector('p').onclick is null
+PASS items[1].types is ['text/html']
+PASS fragment2.documentElement.textContent is "Hello world 2"
+PASS fragment2.querySelector('span') is non-null.
+PASS fragment2.querySelector('p') is null
+PASS successfullyParsed is true
+
+TEST COMPLETE
+

Added: trunk/LayoutTests/editing/async-clipboard/sanitize-when-reading-markup.html (0 => 253486)


--- trunk/LayoutTests/editing/async-clipboard/sanitize-when-reading-markup.html	                        (rev 0)
+++ trunk/LayoutTests/editing/async-clipboard/sanitize-when-reading-markup.html	2019-12-13 18:39:40 UTC (rev 253486)
@@ -0,0 +1,90 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ domPasteAllowed=false useFlexibleViewport=true experimental:AsyncClipboardAPIEnabled=true ] -->
+<html>
+    <meta charset="utf8">
+    <head>
+        <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+        <script src=""
+        <script src=""
+        <script src=""
+        <style>
+            button {
+                display: block;
+                margin: 40px 0;
+            }
+
+            iframe {
+                width: 240px;
+                height: 120px;
+            }
+        </style>
+    </head>
+    <script>
+        jsTestIsAsync = true;
+        finishedCopying = false;
+
+        async function runTest() {
+            description("This test verifies that navigator.clipboard.writeText sanitizes 'text/html'. To manually run the test, click the button in the subframe below to copy HTML, and then click the paste button.");
+
+            const frame = document.querySelector("iframe");
+            const button = document.querySelector("button");
+            button.addEventListener("click", async () => {
+                try {
+                    items = await navigator.clipboard.read();
+                    testPassed("Read items from the clipboard.");
+                    shouldBe("items.length", "2");
+
+                    shouldBe("items[0].types", "['text/html']");
+                    fragment1 = await loadDocument(await items[0].getType("text/html"));
+                    shouldBeEqualToString("fragment1.documentElement.textContent", "Hello world 1");
+                    shouldBeNonNull("fragment1.querySelector('p')");
+                    shouldBeNull("fragment1.querySelector('script')");
+                    shouldBeNull("fragment1.querySelector('p').onclick");
+
+                    shouldBe("items[1].types", "['text/html']");
+                    fragment2 = await loadDocument(await items[1].getType("text/html"));
+                    shouldBeEqualToString("fragment2.documentElement.textContent", "Hello world 2");
+                    shouldBeNonNull("fragment2.querySelector('span')");
+                    shouldBeNull("fragment2.querySelector('p')");
+                } catch (exception) {
+                    testFailed("Did not write to or read from the clipboard.");
+                } finally {
+                    button.remove();
+                    frame.remove();
+                    finishJSTest();
+                }
+            });
+
+            addEventListener("message", event => {
+                testPassed("Wrote markup to the clipboard.");
+                finishedCopying = event.data ="" "finished-copying";
+            });
+
+            if (!window.testRunner)
+                return;
+
+            await UIHelper.activateElement(document.querySelector("iframe"));
+            await new Promise(resolve => shouldBecomeEqual("finishedCopying", "true", resolve));
+            await triggerProgrammaticPaste(button);
+        }
+
+        addEventListener("load", runTest);
+    </script>
+    <body>
+        <iframe src=""
+            <button id='copy' style='font-size: 40px; text-align: center;'>Click to copy</button>
+            <script>
+                const markup1 = `<script>console.log('This script tag should be sanitized out.')</${'script'}><p _onclick_='_javascript_:void()'>Hello world 1</p>`;
+                const markup2 = `<p style='display: none;'>You should not see this text.</p><span>Hello world 2</span>`;
+                copy.addEventListener('click', async () => {
+                    await navigator.clipboard.write([
+                        new ClipboardItem({ 'text/html' : markup1 }),
+                        new ClipboardItem({ 'text/html' : markup2 })
+                    ]);
+                    parent.postMessage('finished-copying', '*');
+                });
+            </script>"></iframe>
+        <button>Click to paste</button>
+        <p id="description"></p>
+        <p id="console"></p>
+    </body>
+</html>

Added: trunk/LayoutTests/editing/async-clipboard/sanitize-when-writing-image-expected.txt (0 => 253486)


--- trunk/LayoutTests/editing/async-clipboard/sanitize-when-writing-image-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/async-clipboard/sanitize-when-writing-image-expected.txt	2019-12-13 18:39:40 UTC (rev 253486)
@@ -0,0 +1,14 @@
+This test verifies that navigator.clipboard.writeText sanitizes 'image/png'. To manually run the test, click the buttons labeled (1), (2), and then (3)
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS Wrote a valid image to the clipboard.
+PASS doneWritingFirstImage became true
+PASS Did not write an invalid image to the clipboard.
+PASS doneWritingSecondImage became true
+PASS Did not write an invalid image to the clipboard.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+

Added: trunk/LayoutTests/editing/async-clipboard/sanitize-when-writing-image.html (0 => 253486)


--- trunk/LayoutTests/editing/async-clipboard/sanitize-when-writing-image.html	                        (rev 0)
+++ trunk/LayoutTests/editing/async-clipboard/sanitize-when-writing-image.html	2019-12-13 18:39:40 UTC (rev 253486)
@@ -0,0 +1,84 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true experimental:AsyncClipboardAPIEnabled=true ] -->
+<html>
+    <meta charset="utf8">
+    <head>
+        <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+        <script src=""
+        <script src=""
+        <script src=""
+        <style>
+            button {
+                display: block;
+                margin: 40px 0;
+            }
+        </style>
+    </head>
+    <script>
+        jsTestIsAsync = true;
+        doneWritingFirstImage = false;
+        doneWritingSecondImage = false;
+
+        async function runTest() {
+            description("This test verifies that navigator.clipboard.writeText sanitizes 'image/png'. To manually run the test, click the buttons labeled (1), (2), and then (3)");
+
+            const writeValidImageButton = document.getElementById("valid");
+            const writeInvalidImageButton = document.getElementById("invalid");
+            const writeBothImagesButton = document.getElementById("both");
+
+            const validImageItem = new ClipboardItem({ "image/png" : imageBlob() });
+            const invalidImageItem = new ClipboardItem({ "image/png" : imageBlob("ThisIsNotActuallyBase64ImageData") });
+
+            writeValidImageButton.addEventListener("click", async () => {
+                try {
+                    await navigator.clipboard.write([validImageItem]);
+                    testPassed("Wrote a valid image to the clipboard.");
+                } catch (exception) {
+                    testFailed("Did not write a valid image to the clipboard.");
+                } finally {
+                    doneWritingFirstImage = true;
+                }
+            });
+
+            writeInvalidImageButton.addEventListener("click", async () => {
+                try {
+                    await navigator.clipboard.write([invalidImageItem]);
+                    testFailed("Should not have written anything to the clipboard.");
+                } catch (exception) {
+                    testPassed("Did not write an invalid image to the clipboard.");
+                } finally {
+                    doneWritingSecondImage = true;
+                }
+            });
+
+            writeBothImagesButton.addEventListener("click", async () => {
+                try {
+                    await navigator.clipboard.write([validImageItem, invalidImageItem]);
+                    testFailed("Should not have written anything to the clipboard.");
+                } catch (exception) {
+                    testPassed("Did not write an invalid image to the clipboard.");
+                } finally {
+                    [writeValidImageButton, writeInvalidImageButton, writeBothImagesButton].map(button => button.remove());
+                    finishJSTest();
+                }
+            });
+
+            if (!window.testRunner)
+                return;
+
+            await UIHelper.activateElement(writeValidImageButton);
+            await new Promise(resolve => shouldBecomeEqual("doneWritingFirstImage", "true", resolve));
+            await UIHelper.activateElement(writeInvalidImageButton);
+            await new Promise(resolve => shouldBecomeEqual("doneWritingSecondImage", "true", resolve));
+            await UIHelper.activateElement(writeBothImagesButton);
+        }
+
+        addEventListener("load", runTest);
+    </script>
+    <body>
+        <button id="valid">1. Write valid item</button>
+        <button id="invalid">2. Write invalid item</button>
+        <button id="both">3. Write valid and invalid items</button>
+        <p id="description"></p>
+        <p id="console"></p>
+    </body>
+</html>

Modified: trunk/LayoutTests/platform/mac-wk1/TestExpectations (253485 => 253486)


--- trunk/LayoutTests/platform/mac-wk1/TestExpectations	2019-12-13 18:35:06 UTC (rev 253485)
+++ trunk/LayoutTests/platform/mac-wk1/TestExpectations	2019-12-13 18:39:40 UTC (rev 253486)
@@ -72,6 +72,7 @@
 editing/async-clipboard/clipboard-do-not-read-text-from-platform-if-text-changes.html [ Skip ]
 editing/async-clipboard/clipboard-read-text-from-platform.html [ Skip ]
 editing/async-clipboard/clipboard-read-text-same-origin.html [ Skip ]
+editing/async-clipboard/sanitize-when-reading-markup.html [ Skip ]
 
 imported/w3c/web-platform-tests/websockets/Close-1000-reason.any.html [ Pass Failure ]
 imported/w3c/web-platform-tests/websockets/Close-1000-reason.any.worker.html [ Pass Failure ]

Modified: trunk/LayoutTests/platform/win/TestExpectations (253485 => 253486)


--- trunk/LayoutTests/platform/win/TestExpectations	2019-12-13 18:35:06 UTC (rev 253485)
+++ trunk/LayoutTests/platform/win/TestExpectations	2019-12-13 18:39:40 UTC (rev 253486)
@@ -1200,6 +1200,8 @@
 webkit.org/b/203100 editing/async-clipboard/clipboard-read-text-from-platform.html [ Skip ]
 webkit.org/b/203100 editing/async-clipboard/clipboard-read-text-same-origin.html [ Skip ]
 webkit.org/b/203100 editing/async-clipboard/clipboard-read-text.html [ Skip ]
+webkit.org/b/203100 editing/async-clipboard/sanitize-when-writing-image.html [ Skip ]
+webkit.org/b/203100 editing/async-clipboard/sanitize-when-reading-markup.html [ Skip ]
 
 webkit.org/b/140783 [ Release ] editing/pasteboard/copy-standalone-image.html [ Failure ImageOnlyFailure ]
 webkit.org/b/140783 [ Debug ] editing/pasteboard/copy-standalone-image.html [ Skip ] # Debug Assertion

Modified: trunk/Source/WebCore/ChangeLog (253485 => 253486)


--- trunk/Source/WebCore/ChangeLog	2019-12-13 18:35:06 UTC (rev 253485)
+++ trunk/Source/WebCore/ChangeLog	2019-12-13 18:39:40 UTC (rev 253486)
@@ -1,3 +1,55 @@
+2019-12-13  Wenson Hsieh  <[email protected]>
+
+        [Clipboard API] Sanitize HTML and image data written using clipboard.write
+        https://bugs.webkit.org/show_bug.cgi?id=205188
+        <rdar://problem/57612968>
+
+        Reviewed by Darin Adler.
+
+        Sanitizes HTML ("text/html") and image ("image/png") content when writing to the platform pasteboard using the
+        clipboard API. See below for more details.
+
+        Tests: editing/async-clipboard/sanitize-when-reading-markup.html
+               editing/async-clipboard/sanitize-when-writing-image.html
+               ClipboardTests.WriteSanitizedMarkup
+
+        * Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp:
+        (WebCore::ClipboardItemBindingsDataSource::ClipboardItemTypeLoader::sanitizeDataIfNeeded):
+
+        Add a new helper method to sanitize `m_data` after loading finishes, but before invoking the completion handler
+        to notify the data source about the clipboard data. Currently, we support writing "text/plain", "text/uri-list",
+        "text/html" and "image/png"; we sanitize HTML by stripping away hidden content such as comments, script, and
+        event listeners, and sanitize image data by painting it into a new graphics context and re-encoding the rendered
+        contents as an image.
+
+        (WebCore::ClipboardItemBindingsDataSource::ClipboardItemTypeLoader::invokeCompletionHandler):
+        * Modules/async-clipboard/ClipboardItemBindingsDataSource.h:
+        * platform/PlatformPasteboard.h:
+        * platform/cocoa/PasteboardCocoa.mm:
+        (WebCore::cocoaTypeToImageType):
+        * platform/ios/PlatformPasteboardIOS.mm:
+        (WebCore::PlatformPasteboard::platformPasteboardTypeForSafeTypeForDOMToReadAndWrite):
+
+        Adjust these helpers to map "image/png" to the platform PNG type on iOS ("public.png"). The extra
+        IncludeImageTypes argument was added to avoid considering "image/png" a web-safe type for writing and reading
+        when exercising DataTransfer APIs, which currently don't support reading the "image/png" MIME type. In the
+        future, we should consider adding support for image sanitization when using DataTransfer.setData or
+        DataTransferItemList.add, and then remove this flag.
+
+        (WebCore::createItemProviderRegistrationList):
+        * platform/mac/LegacyNSPasteboardTypes.h:
+
+        Add an entry for legacyPNGPasteboardType on macOS, and replace one use of it in PasteboardCocoa.mm.
+
+        (WebCore::legacyPNGPasteboardType):
+        * platform/mac/PlatformPasteboardMac.mm:
+        (WebCore::PlatformPasteboard::write):
+        (WebCore::PlatformPasteboard::platformPasteboardTypeForSafeTypeForDOMToReadAndWrite):
+
+        Likewise, map "image/png" to legacyPNGPasteboardType() on macOS, which was added above.
+
+        (WebCore::createPasteboardItem):
+
 2019-12-13  Chris Dumez  <[email protected]>
 
         Behavior of [[GetOwnProperty]] for cross-origin windows is not spec-compliant

Modified: trunk/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp (253485 => 253486)


--- trunk/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp	2019-12-13 18:35:06 UTC (rev 253485)
+++ trunk/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp	2019-12-13 18:39:40 UTC (rev 253486)
@@ -26,6 +26,7 @@
 #include "config.h"
 #include "ClipboardItemBindingsDataSource.h"
 
+#include "BitmapImage.h"
 #include "Blob.h"
 #include "Clipboard.h"
 #include "ClipboardItem.h"
@@ -32,6 +33,8 @@
 #include "Document.h"
 #include "FileReaderLoader.h"
 #include "Frame.h"
+#include "GraphicsContext.h"
+#include "ImageBuffer.h"
 #include "JSBlob.h"
 #include "JSDOMPromise.h"
 #include "JSDOMPromiseDeferred.h"
@@ -252,10 +255,51 @@
     invokeCompletionHandler();
 }
 
+void ClipboardItemBindingsDataSource::ClipboardItemTypeLoader::sanitizeDataIfNeeded()
+{
+    if (m_type == "text/html"_s) {
+        String markupToSanitize;
+        if (WTF::holds_alternative<Ref<SharedBuffer>>(m_data)) {
+            auto& buffer = WTF::get<Ref<SharedBuffer>>(m_data);
+            markupToSanitize = String::fromUTF8(buffer->data(), buffer->size());
+        } else if (WTF::holds_alternative<String>(m_data))
+            markupToSanitize = WTF::get<String>(m_data);
+
+        if (markupToSanitize.isEmpty())
+            return;
+
+        m_data = { sanitizeMarkup(markupToSanitize) };
+    }
+
+    if (m_type == "image/png"_s) {
+        RefPtr<SharedBuffer> bufferToSanitize;
+        if (WTF::holds_alternative<Ref<SharedBuffer>>(m_data))
+            bufferToSanitize = WTF::get<Ref<SharedBuffer>>(m_data).ptr();
+        else if (WTF::holds_alternative<String>(m_data))
+            bufferToSanitize = utf8Buffer(WTF::get<String>(m_data));
+
+        if (!bufferToSanitize || bufferToSanitize->isEmpty())
+            return;
+
+        auto bitmapImage = BitmapImage::create();
+        bitmapImage->setData(WTFMove(bufferToSanitize), true);
+        auto imageBuffer = ImageBuffer::create(bitmapImage->size(), Unaccelerated);
+        if (!imageBuffer) {
+            m_data = { nullString() };
+            return;
+        }
+
+        imageBuffer->context().drawImage(bitmapImage.get(), FloatPoint::zero());
+        m_data = { SharedBuffer::create(imageBuffer->toData("image/png"_s)) };
+    }
+}
+
 void ClipboardItemBindingsDataSource::ClipboardItemTypeLoader::invokeCompletionHandler()
 {
-    if (auto completion = WTFMove(m_completionHandler))
+    if (auto completion = WTFMove(m_completionHandler)) {
+        sanitizeDataIfNeeded();
         completion();
+    }
 }
 
 void ClipboardItemBindingsDataSource::ClipboardItemTypeLoader::didResolveToBlob(ScriptExecutionContext& context, Ref<Blob>&& blob)

Modified: trunk/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.h (253485 => 253486)


--- trunk/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.h	2019-12-13 18:35:06 UTC (rev 253485)
+++ trunk/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.h	2019-12-13 18:39:40 UTC (rev 253486)
@@ -73,6 +73,7 @@
     private:
         ClipboardItemTypeLoader(const String& type, CompletionHandler<void()>&&);
 
+        void sanitizeDataIfNeeded();
         void invokeCompletionHandler();
 
         // FileReaderLoaderClient methods.

Modified: trunk/Source/WebCore/platform/PlatformPasteboard.h (253485 => 253486)


--- trunk/Source/WebCore/platform/PlatformPasteboard.h	2019-12-13 18:35:06 UTC (rev 253485)
+++ trunk/Source/WebCore/platform/PlatformPasteboard.h	2019-12-13 18:39:40 UTC (rev 253486)
@@ -66,7 +66,8 @@
     WEBCORE_EXPORT Optional<Vector<PasteboardItemInfo>> allPasteboardItemInfo(int64_t changeCount);
     WEBCORE_EXPORT static String uniqueName();
 
-    WEBCORE_EXPORT static String platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(const String& domType);
+    enum class IncludeImageTypes : bool { No, Yes };
+    static String platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(const String& domType, IncludeImageTypes = IncludeImageTypes::No);
 
     WEBCORE_EXPORT void getTypes(Vector<String>& types);
     WEBCORE_EXPORT RefPtr<SharedBuffer> bufferForType(const String& pasteboardType);

Modified: trunk/Source/WebCore/platform/cocoa/PasteboardCocoa.mm (253485 => 253486)


--- trunk/Source/WebCore/platform/cocoa/PasteboardCocoa.mm	2019-12-13 18:35:06 UTC (rev 253485)
+++ trunk/Source/WebCore/platform/cocoa/PasteboardCocoa.mm	2019-12-13 18:39:40 UTC (rev 253486)
@@ -68,7 +68,7 @@
     if (cocoaType == String(kUTTypeTIFF))
         return ImageType::TIFF;
 #if PLATFORM(MAC)
-    if (cocoaType == "Apple PNG pasteboard type") // NSPNGPboardType
+    if (cocoaType == String(legacyPNGPasteboardType())) // NSPNGPboardType
         return ImageType::PNG;
 #endif
     if (cocoaType == String(kUTTypePNG))

Modified: trunk/Source/WebCore/platform/ios/PlatformPasteboardIOS.mm (253485 => 253486)


--- trunk/Source/WebCore/platform/ios/PlatformPasteboardIOS.mm	2019-12-13 18:35:06 UTC (rev 253485)
+++ trunk/Source/WebCore/platform/ios/PlatformPasteboardIOS.mm	2019-12-13 18:39:40 UTC (rev 253486)
@@ -322,7 +322,7 @@
     return String();
 }
 
-String PlatformPasteboard::platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(const String& domType)
+String PlatformPasteboard::platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(const String& domType, IncludeImageTypes includeImageTypes)
 {
     if (domType == "text/plain")
         return kUTTypePlainText;
@@ -333,6 +333,9 @@
     if (domType == "text/uri-list")
         return kUTTypeURL;
 
+    if (includeImageTypes == IncludeImageTypes::Yes && domType == "image/png")
+        return kUTTypePNG;
+
     return { };
 }
 
@@ -603,7 +606,7 @@
             return;
 
         NSString *stringValue = value;
-        auto cocoaType = PlatformPasteboard::platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(type).createCFString();
+        auto cocoaType = PlatformPasteboard::platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(type, PlatformPasteboard::IncludeImageTypes::Yes).createCFString();
         if (UTTypeConformsTo(cocoaType.get(), kUTTypeURL))
             [representationsToRegister addRepresentingObject:[NSURL URLWithString:stringValue]];
         else if (UTTypeConformsTo(cocoaType.get(), kUTTypePlainText))

Modified: trunk/Source/WebCore/platform/mac/LegacyNSPasteboardTypes.h (253485 => 253486)


--- trunk/Source/WebCore/platform/mac/LegacyNSPasteboardTypes.h	2019-12-13 18:35:06 UTC (rev 253485)
+++ trunk/Source/WebCore/platform/mac/LegacyNSPasteboardTypes.h	2019-12-13 18:39:40 UTC (rev 253486)
@@ -86,6 +86,11 @@
     return NSFilesPromisePboardType;
 }
 
+inline NSString *legacyPNGPasteboardType()
+{
+    return @"Apple PNG pasteboard type";
+}
+
 } // namespace WebCore
 
 ALLOW_DEPRECATED_DECLARATIONS_END

Modified: trunk/Source/WebCore/platform/mac/PlatformPasteboardMac.mm (253485 => 253486)


--- trunk/Source/WebCore/platform/mac/PlatformPasteboardMac.mm	2019-12-13 18:35:06 UTC (rev 253485)
+++ trunk/Source/WebCore/platform/mac/PlatformPasteboardMac.mm	2019-12-13 18:39:40 UTC (rev 253486)
@@ -226,7 +226,7 @@
 {
     NSMutableArray *types = [NSMutableArray array];
     data.forEachType([&] (auto& type) {
-        NSString *platformType = platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(type);
+        NSString *platformType = platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(type, IncludeImageTypes::Yes);
         if (platformType.length)
             [types addObject:platformType];
     });
@@ -237,11 +237,19 @@
 
     [m_pasteboard declareTypes:types owner:nil];
 
-    data.forEachPlatformString([&] (auto& type, auto& data) {
-        auto platformType = platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(type);
-        ASSERT(!platformType.isEmpty());
-        if (!platformType.isEmpty())
-            [m_pasteboard setString:data forType:platformType];
+    data.forEachPlatformStringOrBuffer([&] (auto& type, auto& stringOrBuffer) {
+        auto platformType = platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(type, IncludeImageTypes::Yes);
+        if (platformType.isEmpty())
+            return;
+
+        if (WTF::holds_alternative<Ref<SharedBuffer>>(stringOrBuffer)) {
+            if (auto platformData = WTF::get<Ref<SharedBuffer>>(stringOrBuffer)->createNSData())
+                [m_pasteboard setData:platformData.get() forType:platformType];
+        } else if (WTF::holds_alternative<String>(stringOrBuffer)) {
+            auto string = WTF::get<String>(stringOrBuffer);
+            if (!!string)
+                [m_pasteboard setString:string forType:platformType];
+        }
     });
 
     if (shouldWriteCustomData) {
@@ -257,7 +265,7 @@
     return [m_pasteboard.get() changeCount];
 }
 
-String PlatformPasteboard::platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(const String& domType)
+String PlatformPasteboard::platformPasteboardTypeForSafeTypeForDOMToReadAndWrite(const String& domType, IncludeImageTypes includeImageTypes)
 {
     if (domType == "text/plain")
         return legacyStringPasteboardType();
@@ -268,6 +276,9 @@
     if (domType == "text/uri-list")
         return legacyURLPasteboardType();
 
+    if (includeImageTypes == IncludeImageTypes::Yes && domType == "image/png")
+        return legacyPNGPasteboardType();
+
     return { };
 }
 
@@ -474,14 +485,13 @@
         if (!platformType)
             return;
 
-        if (WTF::holds_alternative<String>(stringOrBuffer)) {
-            [item setString:WTF::get<String>(stringOrBuffer) forType:platformType];
-            return;
-        }
-
         if (WTF::holds_alternative<Ref<SharedBuffer>>(stringOrBuffer)) {
             if (auto platformData = WTF::get<Ref<SharedBuffer>>(stringOrBuffer)->createNSData())
                 [item setData:platformData.get() forType:platformType];
+        } else if (WTF::holds_alternative<String>(stringOrBuffer)) {
+            auto string = WTF::get<String>(stringOrBuffer);
+            if (!!string)
+                [item setString:string forType:platformType];
         }
     });
 

Modified: trunk/Tools/ChangeLog (253485 => 253486)


--- trunk/Tools/ChangeLog	2019-12-13 18:35:06 UTC (rev 253485)
+++ trunk/Tools/ChangeLog	2019-12-13 18:39:40 UTC (rev 253486)
@@ -1,3 +1,19 @@
+2019-12-13  Wenson Hsieh  <[email protected]>
+
+        [Clipboard API] Sanitize HTML and image data written using clipboard.write
+        https://bugs.webkit.org/show_bug.cgi?id=205188
+        <rdar://problem/57612968>
+
+        Reviewed by Darin Adler.
+
+        Adds an API test to verify that the markup written to the platform pasteboard on macOS and iOS is sanitized, and
+        does not contain hidden content, such as script elements.
+
+        * TestWebKitAPI/Tests/WebKitCocoa/ClipboardTests.mm:
+        (-[TestWKWebView writeString:toClipboardWithType:]):
+        (readMarkupFromPasteboard):
+        * TestWebKitAPI/Tests/WebKitCocoa/clipboard.html:
+
 2019-12-13  Kate Cheney  <[email protected]>
 
         Create WebKit API calls for ITP Data

Modified: trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/ClipboardTests.mm (253485 => 253486)


--- trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/ClipboardTests.mm	2019-12-13 18:35:06 UTC (rev 253485)
+++ trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/ClipboardTests.mm	2019-12-13 18:39:40 UTC (rev 253486)
@@ -29,6 +29,7 @@
 #import "TestWKWebView.h"
 #import "UIKitSPI.h"
 #import <CoreServices/CoreServices.h>
+#import <WebCore/LegacyNSPasteboardTypes.h>
 #import <WebKit/WKPreferencesPrivate.h>
 #import <WebKit/_WKExperimentalFeature.h>
 
@@ -50,6 +51,16 @@
     TestWebKitAPI::Util::run(&done);
 }
 
+- (void)writeString:(NSString *)string toClipboardWithType:(NSString *)type
+{
+    __block bool done = false;
+    [self performAfterReceivingMessage:@"wroteStringToClipboard" action:^{
+        done = true;
+    }];
+    [self evaluateJavaScript:[NSString stringWithFormat:@"writeStringToClipboard(`%@`, `%@`)", type, string] completionHandler:nil];
+    TestWebKitAPI::Util::run(&done);
+}
+
 @end
 
 static RetainPtr<TestWKWebView> createWebViewForClipboardTests()
@@ -118,6 +129,16 @@
 #endif
 }
 
+static NSString *readMarkupFromPasteboard()
+{
+#if PLATFORM(MAC)
+    NSData *rawData = [NSPasteboard.generalPasteboard dataForType:WebCore::legacyHTMLPasteboardType()];
+#elif PLATFORM(IOS)
+    NSData *rawData = [UIPasteboard.generalPasteboard dataForPasteboardType:(__bridge NSString *)kUTTypeHTML];
+#endif
+    return [[[NSString alloc] initWithData:rawData encoding:NSUTF8StringEncoding] autorelease];
+}
+
 TEST(ClipboardTests, ReadMultipleItems)
 {
     auto webView = createWebViewForClipboardTests();
@@ -133,3 +154,14 @@
     EXPECT_WK_STREQ("https://webkit.org/", [webView objectByEvaluatingJavaScript:@"clipboardData[3]['text/uri-list']"]);
     EXPECT_WK_STREQ("https://webkit.org/", [webView stringByEvaluatingJavaScript:@"clipboardData[3]['text/html'].querySelector('a').href"]);
 }
+
+TEST(ClipboardTests, WriteSanitizedMarkup)
+{
+    auto webView = createWebViewForClipboardTests();
+    [webView writeString:@"<script>/* super secret */</script>This is a test." toClipboardWithType:@"text/html"];
+
+    NSString *writtenMarkup = readMarkupFromPasteboard();
+    EXPECT_TRUE([writtenMarkup containsString:@"This is a test."]);
+    EXPECT_FALSE([writtenMarkup containsString:@"super secret"]);
+    EXPECT_FALSE([writtenMarkup containsString:@"<script>"]);
+}

Modified: trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/clipboard.html (253485 => 253486)


--- trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/clipboard.html	2019-12-13 18:35:06 UTC (rev 253485)
+++ trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/clipboard.html	2019-12-13 18:39:40 UTC (rev 253486)
@@ -28,6 +28,18 @@
 clipboardData = [];
 exception = null;
 
+async function writeStringToClipboard(type, string) {
+    try {
+        const itemData = {};
+        itemData[type] = string;
+        await navigator.clipboard.write([new ClipboardItem(itemData)]);
+    } catch (e) {
+        exception = e;
+    } finally {
+        webkit.messageHandlers.testHandler.postMessage("wroteStringToClipboard");
+    }
+}
+
 async function readClipboard() {
     try {
         const items = await navigator.clipboard.read();
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to