diff options
author | Raymes Khoury <raymes@chromium.org> | 2015-09-02 10:20:49 +1000 |
---|---|---|
committer | Raymes Khoury <raymes@chromium.org> | 2015-09-02 00:21:57 +0000 |
commit | 398696033a3f2e954aea68fdec10becb836a80b4 (patch) | |
tree | 31ae7b57d083a050c16628dd52d27cfcc1952b34 | |
parent | a9db9721cf2c1c1f7373e123718e1433419d3598 (diff) | |
download | chromium_src-398696033a3f2e954aea68fdec10becb836a80b4.zip chromium_src-398696033a3f2e954aea68fdec10becb836a80b4.tar.gz chromium_src-398696033a3f2e954aea68fdec10becb836a80b4.tar.bz2 |
Prevent leaking PDF data cross-origin
BUG=520422
Review URL: https://codereview.chromium.org/1311973002
Cr-Commit-Position: refs/heads/master@{#345267}
(cherry picked from commit fff450abc4e2fb330ba700547a8e6a7b0fb90a6e)
Review URL: https://codereview.chromium.org/1316803003 .
Cr-Commit-Position: refs/branch-heads/2454@{#446}
Cr-Branched-From: 12bfc3360892ec53cd00fc239a47e5298beb063b-refs/heads/master@{#338390}
-rw-r--r-- | chrome/browser/pdf/pdf_extension_test.cc | 94 | ||||
-rw-r--r-- | chrome/browser/resources/pdf/pdf.js | 19 | ||||
-rw-r--r-- | pdf/out_of_process_instance.cc | 31 |
3 files changed, 126 insertions, 18 deletions
diff --git a/chrome/browser/pdf/pdf_extension_test.cc b/chrome/browser/pdf/pdf_extension_test.cc index d6718e7..aec0754 100644 --- a/chrome/browser/pdf/pdf_extension_test.cc +++ b/chrome/browser/pdf/pdf_extension_test.cc @@ -26,6 +26,15 @@ const int kNumberLoadTestParts = 10; +bool GetGuestCallback(content::WebContents** guest_out, + content::WebContents* guest) { + EXPECT_FALSE(*guest_out); + *guest_out = guest; + // Return false so that we iterate through all the guests and verify there is + // only one. + return false; +} + class PDFExtensionTest : public ExtensionApiTest, public testing::WithParamInterface<int> { public: @@ -156,6 +165,48 @@ class PDFExtensionTest : public ExtensionApiTest, // someone deleting the directory and silently making the test pass. ASSERT_GE(count, 1u); } + + void TestGetSelectedTextReply(GURL url, bool expect_success) { + ui_test_utils::NavigateToURL(browser(), url); + content::WebContents* web_contents = + browser()->tab_strip_model()->GetActiveWebContents(); + ASSERT_TRUE(pdf_extension_test_util::EnsurePDFHasLoaded(web_contents)); + + // Reach into the guest and hook into it such that it posts back a 'flush' + // message after every getSelectedTextReply message sent. + content::BrowserPluginGuestManager* guest_manager = + web_contents->GetBrowserContext()->GetGuestManager(); + content::WebContents* guest_contents = nullptr; + ASSERT_NO_FATAL_FAILURE(guest_manager->ForEachGuest( + web_contents, base::Bind(&GetGuestCallback, &guest_contents))); + ASSERT_TRUE(guest_contents); + ASSERT_TRUE(content::ExecuteScript( + guest_contents, + "var oldSendScriptingMessage = " + " PDFViewer.prototype.sendScriptingMessage_;" + "PDFViewer.prototype.sendScriptingMessage_ = function(message) {" + " oldSendScriptingMessage.bind(this)(message);" + " if (message.type == 'getSelectedTextReply')" + " this.parentWindow_.postMessage('flush', '*');" + "}")); + + // Add an event listener for flush messages and request the selected text. + // If we get a flush message without receiving getSelectedText we know that + // the message didn't come through. + bool success = false; + ASSERT_TRUE(content::ExecuteScriptAndExtractBool( + web_contents, + "window.addEventListener('message', function(event) {" + " if (event.data == 'flush')" + " window.domAutomationController.send(false);" + " if (event.data.type == 'getSelectedTextReply')" + " window.domAutomationController.send(true);" + "});" + "document.getElementsByTagName('embed')[0].postMessage(" + " {type: 'getSelectedText'});", + &success)); + ASSERT_EQ(expect_success, success); + } }; IN_PROC_BROWSER_TEST_P(PDFExtensionTest, Load) { @@ -200,6 +251,49 @@ IN_PROC_BROWSER_TEST_F(PDFExtensionTest, ZoomManager) { RunTestsInFile("zoom_manager_test.js", "test.pdf"); } +// Ensure that the internal PDF plugin application/x-google-chrome-pdf won't be +// loaded if it's not loaded in the chrome extension page. +IN_PROC_BROWSER_TEST_F(PDFExtensionTest, EnsureInternalPluginDisabled) { + std::string url = embedded_test_server()->GetURL("/pdf/test.pdf").spec(); + std::string data_url = + "data:text/html," + "<html><body>" + "<embed type=\"application/x-google-chrome-pdf\" src=\"" + + url + + "\">" + "</body></html>"; + ui_test_utils::NavigateToURL(browser(), GURL(data_url)); + content::WebContents* web_contents = + browser()->tab_strip_model()->GetActiveWebContents(); + bool plugin_loaded = false; + ASSERT_TRUE(content::ExecuteScriptAndExtractBool( + web_contents, + "var plugin_loaded = " + " document.getElementsByTagName('embed')[0].postMessage !== undefined;" + "window.domAutomationController.send(plugin_loaded);", + &plugin_loaded)); + ASSERT_FALSE(plugin_loaded); +} + +// Ensure cross-origin replies won't work for getSelectedText. +IN_PROC_BROWSER_TEST_F(PDFExtensionTest, EnsureCrossOriginRepliesBlocked) { + std::string url = embedded_test_server()->GetURL("/pdf/test.pdf").spec(); + std::string data_url = + "data:text/html," + "<html><body>" + "<embed type=\"application/pdf\" src=\"" + + url + + "\">" + "</body></html>"; + TestGetSelectedTextReply(GURL(data_url), false); +} + +// Ensure same-origin replies do work for getSelectedText. +IN_PROC_BROWSER_TEST_F(PDFExtensionTest, EnsureSameOriginRepliesAllowed) { + TestGetSelectedTextReply(embedded_test_server()->GetURL("/pdf/test.pdf"), + true); +} + class MaterialPDFExtensionTest : public PDFExtensionTest { void SetUpCommandLine(base::CommandLine* command_line) override { command_line->AppendSwitch(switches::kEnablePdfMaterialUI); diff --git a/chrome/browser/resources/pdf/pdf.js b/chrome/browser/resources/pdf/pdf.js index 43e085e..c497574 100644 --- a/chrome/browser/resources/pdf/pdf.js +++ b/chrome/browser/resources/pdf/pdf.js @@ -84,6 +84,7 @@ function PDFViewer(browserApi) { this.browserApi_ = browserApi; this.loadState_ = LoadState.LOADING; this.parentWindow_ = null; + this.parentOrigin_ = null; this.delayedScriptingMessages_ = []; @@ -674,6 +675,7 @@ PDFViewer.prototype = { handleScriptingMessage: function(message) { if (this.parentWindow_ != message.source) { this.parentWindow_ = message.source; + this.parentOrigin_ = message.origin; // Ensure that we notify the embedder if the document is loaded. if (this.loadState_ != LoadState.LOADING) this.sendDocumentLoadedMessage_(); @@ -760,11 +762,22 @@ PDFViewer.prototype = { * @param {Object} message the message to send. */ sendScriptingMessage_: function(message) { - if (this.parentWindow_) - this.parentWindow_.postMessage(message, '*'); + if (this.parentWindow_ && this.parentOrigin_) { + var targetOrigin; + // Only send data back to the embedder if it is from the same origin, + // unless we're sending it to ourselves (which could happen in the case + // of tests). We also allow documentLoaded messages through as this won't + // leak important information. + if (this.parentOrigin_ == window.location.origin) + targetOrigin = this.parentOrigin_; + else if (message.type == 'documentLoaded') + targetOrigin = '*'; + else + targetOrigin = this.browserApi_.getStreamInfo().originalUrl; + this.parentWindow_.postMessage(message, targetOrigin); + } }, - /** * @type {Viewport} the viewport of the PDF viewer. */ diff --git a/pdf/out_of_process_instance.cc b/pdf/out_of_process_instance.cc index df881c5..808606b 100644 --- a/pdf/out_of_process_instance.cc +++ b/pdf/out_of_process_instance.cc @@ -303,23 +303,24 @@ bool OutOfProcessInstance::Init(uint32_t argc, const char* argn[], const char* argv[]) { // Check if the PDF is being loaded in the PDF chrome extension. We only allow - // the plugin to be put into "full frame" mode when it is being loaded in the - // extension because this enables some features that we don't want pages - // abusing outside of the extension. + // the plugin to be loaded in the extension and print preview to avoid + // exposing sensitive APIs directly to external websites. pp::Var document_url_var = pp::URLUtil_Dev::Get()->GetDocumentURL(this); - std::string document_url = document_url_var.is_string() ? - document_url_var.AsString() : std::string(); + if (!document_url_var.is_string()) + return false; + std::string document_url = document_url_var.AsString(); std::string extension_url = std::string(kChromeExtension); - bool in_extension = - !document_url.compare(0, extension_url.size(), extension_url); - - if (in_extension) { - // Check if the plugin is full frame. This is passed in from JS. - for (uint32_t i = 0; i < argc; ++i) { - if (strcmp(argn[i], "full-frame") == 0) { - full_ = true; - break; - } + std::string print_preview_url = std::string(kChromePrint); + if (!base::StringPiece(document_url).starts_with(kChromeExtension) && + !base::StringPiece(document_url).starts_with(kChromePrint)) { + return false; + } + + // Check if the plugin is full frame. This is passed in from JS. + for (uint32_t i = 0; i < argc; ++i) { + if (strcmp(argn[i], "full-frame") == 0) { + full_ = true; + break; } } |