Bug 713980 - Log warnings about blocked cross-site requests to the Web Console. r=smaug
authorGarrett Robinson <grobinson@mozilla.com>
Tue, 07 Jan 2014 09:51:05 -0500
changeset 162336 7e7ae8cbd0231a0e5b5bede32359ea523bfd9825
parent 162335 7aaa3c22fffc0b6ffd5fd7ab279406dd148c4791
child 162337 403ab3213879eed64e81fcd4fc93e40bc4d7b92b
push id38175
push userryanvm@gmail.com
push dateTue, 07 Jan 2014 14:59:00 +0000
treeherdermozilla-inbound@ea1cb5463063 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs713980
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 713980 - Log warnings about blocked cross-site requests to the Web Console. r=smaug
browser/devtools/webconsole/webconsole.js
content/base/src/nsCrossSiteListenerProxy.cpp
content/base/test/mochitest.ini
content/base/test/test_warning_for_blocked_cross_site_request.html
dom/locales/en-US/chrome/security/security.properties
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -4439,16 +4439,17 @@ var Utils = {
         return CATEGORY_CSS;
 
       case "Mixed Content Blocker":
       case "Mixed Content Message":
       case "CSP":
       case "Invalid HSTS Headers":
       case "Insecure Password Field":
       case "SSL":
+      case "CORS":
         return CATEGORY_SECURITY;
 
       default:
         return CATEGORY_JS;
     }
   },
 
   /**
--- a/content/base/src/nsCrossSiteListenerProxy.cpp
+++ b/content/base/src/nsCrossSiteListenerProxy.cpp
@@ -1,9 +1,9 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/Assertions.h"
 #include "mozilla/LinkedList.h"
 
 #include "nsCrossSiteListenerProxy.h"
@@ -21,25 +21,100 @@
 #include "nsIChannelEventSink.h"
 #include "nsIAsyncVerifyRedirectCallback.h"
 #include "nsCharSeparatedTokenizer.h"
 #include "nsAsyncRedirectVerifyHelper.h"
 #include "nsClassHashtable.h"
 #include "nsHashKeys.h"
 #include "nsStreamUtils.h"
 #include "mozilla/Preferences.h"
+#include "nsIScriptError.h"
+#include "nsILoadGroup.h"
+#include "nsILoadContext.h"
+#include "nsIConsoleService.h"
+#include "nsIDOMWindowUtils.h"
+#include "nsIDOMWindow.h"
 #include <algorithm>
 
 using namespace mozilla;
 
 #define PREFLIGHT_CACHE_SIZE 100
 
 static bool gDisableCORS = false;
 static bool gDisableCORSPrivateData = false;
 
+static nsresult
+LogBlockedRequest(nsIRequest* aRequest)
+{
+  nsresult rv = NS_OK;
+
+  // Get the innerWindowID associated with the XMLHTTPRequest
+  PRUint64 innerWindowID = 0;
+
+  nsCOMPtr<nsILoadGroup> loadGroup;
+  aRequest->GetLoadGroup(getter_AddRefs(loadGroup));
+  if (loadGroup) {
+    nsCOMPtr<nsIInterfaceRequestor> callbacks;
+    loadGroup->GetNotificationCallbacks(getter_AddRefs(callbacks));
+    if (callbacks) {
+      nsCOMPtr<nsILoadContext> loadContext = do_GetInterface(callbacks);
+      if(loadContext) {
+        nsCOMPtr<nsIDOMWindow> window;
+        loadContext->GetAssociatedWindow(getter_AddRefs(window));
+        if (window) {
+          nsCOMPtr<nsIDOMWindowUtils> du = do_GetInterface(window);
+          du->GetCurrentInnerWindowID(&innerWindowID);
+        }
+      }
+    }
+  }
+
+  if (!innerWindowID) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+  nsCOMPtr<nsIURI> aUri;
+  channel->GetURI(getter_AddRefs(aUri));
+  nsAutoCString spec;
+  if (aUri) {
+    aUri->GetSpec(spec);
+  }
+
+  // Generate the error message
+  nsXPIDLString blockedMessage;
+  NS_ConvertUTF8toUTF16 specUTF16(spec);
+  const PRUnichar* params[] = { specUTF16.get() };
+  rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eSECURITY_PROPERTIES,
+                                             "CrossSiteRequestBlocked",
+                                             params,
+                                             blockedMessage);
+
+  // Build the error object and log it to the console
+  nsCOMPtr<nsIConsoleService> console(do_GetService(NS_CONSOLESERVICE_CONTRACTID, &rv));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCOMPtr<nsIScriptError> scriptError = do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsAutoString msg(blockedMessage.get());
+  rv = scriptError->InitWithWindowID(msg,
+                                     NS_ConvertUTF8toUTF16(spec),
+                                     EmptyString(),
+                                     0,
+                                     0,
+                                     nsIScriptError::warningFlag,
+                                     "CORS",
+                                     innerWindowID);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = console->LogMessage(scriptError);
+  return rv;
+}
+
 //////////////////////////////////////////////////////////////////////////
 // Preflight cache
 
 class nsPreflightCache
 {
 public:
   struct TokenTime
   {
@@ -393,18 +468,21 @@ nsCORSListenerProxy::Init(nsIChannel* aC
   }
   return rv;
 }
 
 NS_IMETHODIMP
 nsCORSListenerProxy::OnStartRequest(nsIRequest* aRequest,
                                     nsISupports* aContext)
 {
-  mRequestApproved = NS_SUCCEEDED(CheckRequestApproved(aRequest));
+  nsresult rv = CheckRequestApproved(aRequest);
+  mRequestApproved = NS_SUCCEEDED(rv);
   if (!mRequestApproved) {
+    rv = LogBlockedRequest(aRequest);
+    NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Failed to log blocked cross-site request");
     if (sPreflightCache) {
       nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
       if (channel) {
         nsCOMPtr<nsIURI> uri;
         NS_GetFinalChannelURI(channel, getter_AddRefs(uri));
         if (uri) {
           // OK to use mRequestingPrincipal since preflights never get
           // redirected.
@@ -617,16 +695,19 @@ nsCORSListenerProxy::AsyncOnChannelRedir
                                             nsIChannel *aNewChannel,
                                             uint32_t aFlags,
                                             nsIAsyncVerifyRedirectCallback *cb)
 {
   nsresult rv;
   if (!NS_IsInternalSameURIRedirect(aOldChannel, aNewChannel, aFlags)) {
     rv = CheckRequestApproved(aOldChannel);
     if (NS_FAILED(rv)) {
+      rv = LogBlockedRequest(aOldChannel);
+      NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Failed to log blocked cross-site request");
+
       if (sPreflightCache) {
         nsCOMPtr<nsIURI> oldURI;
         NS_GetFinalChannelURI(aOldChannel, getter_AddRefs(oldURI));
         if (oldURI) {
           // OK to use mRequestingPrincipal since preflights never get
           // redirected.
           sPreflightCache->RemoveEntries(oldURI, mRequestingPrincipal);
         }
--- a/content/base/test/mochitest.ini
+++ b/content/base/test/mochitest.ini
@@ -575,8 +575,9 @@ support-files =
 [test_x-frame-options.html]
 [test_xbl_userdata.xhtml]
 [test_xhr_abort_after_load.html]
 [test_xhr_forbidden_headers.html]
 [test_xhr_progressevents.html]
 [test_xhr_send_readystate.html]
 [test_xhr_withCredentials.html]
 [test_file_from_blob.html]
+[test_warning_for_blocked_cross_site_request.html]
new file mode 100644
--- /dev/null
+++ b/content/base/test/test_warning_for_blocked_cross_site_request.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=713980
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 713980</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+  <!-- Load a cross-origin webfont without CORS (common pain point -->
+  <style>
+    @font-face {
+      font-family: "bad_cross_origin_webfont";
+      src: url('http://example.org/tests/content/base/test/csp/file_CSP.sjs?testid=font_bad&type=application/octet-stream');
+    }
+    div#bad_webfont { font-family: "bad_cross_origin_webfont"; }
+  </style>
+</head>
+<body>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var tests = {
+  font : {
+    uri_test : "font_bad",
+    result : null
+  },
+  xhr : {
+    uri_test : "http://invalid",
+    result : null
+  },
+}
+
+function testsComplete() {
+  for (var testName in tests) {
+    var test = tests[testName];
+    if (test.result == null)
+      return false;
+  }
+  return true;
+}
+
+SpecialPowers.registerConsoleListener(function CORSMsgListener(aMsg) {
+  if (!/Cross-Origin Request Blocked/.test(aMsg.message))
+    return;
+
+  for (var testName in tests) {
+    var test = tests[testName];
+    if (test.result != null)
+      continue;
+
+    var testRegexp = new RegExp(test.uri_test);
+    if (testRegexp.test(aMsg.message)) {
+      test.result = true;
+      ok(true, "Got \"Cross-site request blocked\" warning message for " + testName);
+      ok(aMsg.category == "CORS", "Got warning message with category \"" + aMsg.category + "\", expected \"CORS\"");
+      // Got the message we wanted - make sure it is destined for a valid inner window
+      ok(aMsg.windowID != 0, "Valid (non-zero) windowID for the cross-site request blocked message.");
+      break;
+    }
+  }
+
+  if (testsComplete()) {
+    SimpleTest.executeSoon(cleanup);
+  }
+});
+
+function cleanup() {
+  SpecialPowers.postConsoleSentinel();
+  SimpleTest.finish();
+}
+
+// Send a cross-origin XHR request without CORS
+var xhr = new XMLHttpRequest();
+xhr.open("GET", "http://example.org/tests/content/base/test/file_CrossSiteXHR_server.sjs?allowOrigin=http://invalid", true);
+xhr.send(null);
+
+// Create a div that triggers a cross-origin webfont request
+// We do this in Javascript in order to guarantee the console listener has
+// already been registered; otherwise, there could be a race.
+var badDiv = document.createElement('div');
+badDiv.setAttribute('id', 'bad_webfont');
+document.body.appendChild(badDiv);
+</script>
+
+</pre>
+</body>
+</html>
--- a/dom/locales/en-US/chrome/security/security.properties
+++ b/dom/locales/en-US/chrome/security/security.properties
@@ -4,15 +4,19 @@ BlockMixedDisplayContent = Blocked loadi
 BlockMixedActiveContent = Blocked loading mixed active content "%1$S"
 
 # CSP
 ReportOnlyCSPIgnored=Report-only CSP policy will be ignored because there are other non-report-only CSP policies applied.
 # LOCALIZATION NOTE: Do not translate "X-Content-Security-Policy", "X-Content-Security-Policy-Report-Only",  "Content-Security-Policy" or "Content-Security-Policy-Report-Only"
 OldCSPHeaderDeprecated=The X-Content-Security-Policy and X-Content-Security-Report-Only headers will be deprecated in the future. Please use the Content-Security-Policy and Content-Security-Report-Only headers with CSP spec compliant syntax instead.
 # LOCALIZATION NOTE: Do not translate "X-Content-Security-Policy/Report-Only" or "Content-Security-Policy/Report-Only"
 BothCSPHeadersPresent=This site specified both an X-Content-Security-Policy/Report-Only header and a Content-Security-Policy/Report-Only header. The X-Content-Security-Policy/Report-Only header(s) will be ignored.
+
+# CORS
+CrossSiteRequestBlocked=Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at %1$S. This can be fixed by moving the resource to the same domain or enabling CORS.
+
 # LOCALIZATION NOTE: Do not translate "Strict-Transport-Security" or "HSTS"
 InvalidSTSHeaders=The site specified an invalid Strict-Transport-Security header.
 InsecurePasswordsPresentOnPage=Password fields present on an insecure (http://) page. This is a security risk that allows user login credentials to be stolen.
 InsecureFormActionPasswordsPresent=Password fields present in a form with an insecure (http://) form action. This is a security risk that allows user login credentials to be stolen.
 InsecurePasswordsPresentOnIframe=Password fields present on an insecure (http://) iframe. This is a security risk that allows user login credentials to be stolen.
 LoadingMixedActiveContent=Loading mixed (insecure) active content on a secure page "%1$S"
 LoadingMixedDisplayContent=Loading mixed (insecure) display content on a secure page "%1$S"