Bug 1024557 - Ignore x-frame-options if CSP with frame-ancestors exists. r=smaug
authorChristoph Kerschbaumer <ckerschb@christophkerschbaumer.com>
Wed, 07 Jun 2017 21:17:49 +0200
changeset 397953 68517278b72ef27b1b14c15c5c7155b4386a5257
parent 397952 06967108134bd25250440c4e7429484856f811be
child 397954 38e06c672764234f76bd25fb482b58c640f09e1b
push idunknown
push userunknown
push dateunknown
reviewerssmaug
bugs1024557
milestone55.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 1024557 - Ignore x-frame-options if CSP with frame-ancestors exists. r=smaug
docshell/base/nsDSURIContentListener.cpp
docshell/base/nsDSURIContentListener.h
dom/base/nsDocument.cpp
dom/base/nsDocument.h
dom/interfaces/security/nsIContentSecurityPolicy.idl
dom/locales/en-US/chrome/security/csp.properties
dom/security/nsCSPContext.cpp
--- a/docshell/base/nsDSURIContentListener.cpp
+++ b/docshell/base/nsDSURIContentListener.cpp
@@ -18,16 +18,17 @@
 #include "nsIScriptSecurityManager.h"
 #include "nsError.h"
 #include "nsCharSeparatedTokenizer.h"
 #include "nsIConsoleService.h"
 #include "nsIScriptError.h"
 #include "nsDocShellLoadTypes.h"
 #include "nsIMultiPartChannel.h"
 #include "nsContentUtils.h"
+#include "mozilla/dom/nsCSPUtils.h"
 
 using namespace mozilla;
 
 nsDSURIContentListener::nsDSURIContentListener(nsDocShell* aDocShell)
   : mDocShell(aDocShell)
   , mExistingJPEGRequest(nullptr)
   , mParentContentListener(nullptr)
 {
@@ -80,24 +81,16 @@ nsDSURIContentListener::DoContent(const 
                                   nsIRequest* aRequest,
                                   nsIStreamListener** aContentHandler,
                                   bool* aAbortProcess)
 {
   nsresult rv;
   NS_ENSURE_ARG_POINTER(aContentHandler);
   NS_ENSURE_TRUE(mDocShell, NS_ERROR_FAILURE);
 
-  // Check whether X-Frame-Options permits us to load this content in an
-  // iframe and abort the load (unless we've disabled x-frame-options
-  // checking).
-  if (!CheckFrameOptions(aRequest)) {
-    *aAbortProcess = true;
-    return NS_OK;
-  }
-
   *aAbortProcess = false;
 
   // determine if the channel has just been retargeted to us...
   nsLoadFlags loadFlags = 0;
   nsCOMPtr<nsIChannel> aOpenedChannel = do_QueryInterface(aRequest);
 
   if (aOpenedChannel) {
     aOpenedChannel->GetLoadFlags(&loadFlags);
@@ -261,19 +254,20 @@ nsDSURIContentListener::SetParentContent
     }
   } else {
     mWeakParentContentListener = nullptr;
     mParentContentListener = nullptr;
   }
   return NS_OK;
 }
 
-bool
+/* static */ bool
 nsDSURIContentListener::CheckOneFrameOptionsPolicy(nsIHttpChannel* aHttpChannel,
-                                                   const nsAString& aPolicy)
+                                                   const nsAString& aPolicy,
+                                                   nsIDocShell* aDocShell)
 {
   static const char allowFrom[] = "allow-from";
   const uint32_t allowFromLen = ArrayLength(allowFrom) - 1;
   bool isAllowFrom =
     StringHead(aPolicy, allowFromLen).LowerCaseEqualsLiteral(allowFrom);
 
   // return early if header does not have one of the values with meaning
   if (!aPolicy.LowerCaseEqualsLiteral("deny") &&
@@ -281,25 +275,25 @@ nsDSURIContentListener::CheckOneFrameOpt
       !isAllowFrom) {
     return true;
   }
 
   nsCOMPtr<nsIURI> uri;
   aHttpChannel->GetURI(getter_AddRefs(uri));
 
   // XXXkhuey when does this happen?  Is returning true safe here?
-  if (!mDocShell) {
+  if (!aDocShell) {
     return true;
   }
 
   // We need to check the location of this window and the location of the top
   // window, if we're not the top.  X-F-O: SAMEORIGIN requires that the
   // document must be same-origin with top window.  X-F-O: DENY requires that
   // the document must never be framed.
-  nsCOMPtr<nsPIDOMWindowOuter> thisWindow = mDocShell->GetWindow();
+  nsCOMPtr<nsPIDOMWindowOuter> thisWindow = aDocShell->GetWindow();
   // If we don't have DOMWindow there is no risk of clickjacking
   if (!thisWindow) {
     return true;
   }
 
   // GetScriptableTop, not GetTop, because we want this to respect
   // <iframe mozbrowser> boundaries.
   nsCOMPtr<nsPIDOMWindowOuter> topWindow = thisWindow->GetScriptableTop();
@@ -309,17 +303,17 @@ nsDSURIContentListener::CheckOneFrameOpt
     return true;
   }
 
   // Find the top docshell in our parent chain that doesn't have the system
   // principal and use it for the principal comparison.  Finding the top
   // content-type docshell doesn't work because some chrome documents are
   // loaded in content docshells (see bug 593387).
   nsCOMPtr<nsIDocShellTreeItem> thisDocShellItem(
-    do_QueryInterface(static_cast<nsIDocShell*>(mDocShell)));
+    do_QueryInterface(static_cast<nsIDocShell*>(aDocShell)));
   nsCOMPtr<nsIDocShellTreeItem> parentDocShellItem;
   nsCOMPtr<nsIDocShellTreeItem> curDocShellItem = thisDocShellItem;
   nsCOMPtr<nsIDocument> topDoc;
   nsresult rv;
   nsCOMPtr<nsIScriptSecurityManager> ssm =
     do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv);
   if (!ssm) {
     MOZ_CRASH();
@@ -398,32 +392,76 @@ nsDSURIContentListener::CheckOneFrameOpt
       ReportXFOViolation(curDocShellItem, uri, eALLOWFROM);
       return false;
     }
   }
 
   return true;
 }
 
+// Ignore x-frame-options if CSP with frame-ancestors exists
+static bool
+ShouldIgnoreFrameOptions(nsIChannel* aChannel, nsIPrincipal* aPrincipal)
+{
+  NS_ENSURE_TRUE(aChannel, false);
+  NS_ENSURE_TRUE(aPrincipal, false);
+
+  nsCOMPtr<nsIContentSecurityPolicy> csp;
+  aPrincipal->GetCsp(getter_AddRefs(csp));
+  if (!csp) {
+    // if there is no CSP, then there is nothing to do here
+    return false;
+  }
+
+  bool enforcesFrameAncestors = false;
+  csp->GetEnforcesFrameAncestors(&enforcesFrameAncestors);
+  if (!enforcesFrameAncestors) {
+    // if CSP does not contain frame-ancestors, then there
+    // is nothing to do here.
+    return false;
+  }
+
+  // log warning to console that xfo is ignored because of CSP
+  nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo();
+  uint64_t innerWindowID = loadInfo ? loadInfo->GetInnerWindowID() : 0;
+  const char16_t* params[] = { u"x-frame-options",
+                               u"frame-ancestors" };
+  CSP_LogLocalizedStr(u"IgnoringSrcBecauseOfDirective",
+                      params, ArrayLength(params),
+                      EmptyString(), // no sourcefile
+                      EmptyString(), // no scriptsample
+                      0,             // no linenumber
+                      0,             // no columnnumber
+                      nsIScriptError::warningFlag,
+                      "CSP", innerWindowID);
+
+  return true;
+}
+
 // Check if X-Frame-Options permits this document to be loaded as a subdocument.
 // This will iterate through and check any number of X-Frame-Options policies
 // in the request (comma-separated in a header, multiple headers, etc).
-bool
-nsDSURIContentListener::CheckFrameOptions(nsIRequest* aRequest)
+/* static */ bool
+nsDSURIContentListener::CheckFrameOptions(nsIChannel* aChannel,
+                                          nsIDocShell* aDocShell,
+                                          nsIPrincipal* aPrincipal)
 {
-  nsresult rv;
-  nsCOMPtr<nsIChannel> chan = do_QueryInterface(aRequest);
-  if (!chan) {
+  if (!aChannel || !aDocShell) {
     return true;
   }
 
-  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(chan);
+  if (ShouldIgnoreFrameOptions(aChannel, aPrincipal)) {
+    return true;
+  }
+
+  nsresult rv;
+  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
   if (!httpChannel) {
     // check if it is hiding in a multipart channel
-    rv = mDocShell->GetHttpChannel(chan, getter_AddRefs(httpChannel));
+    rv = nsDocShell::Cast(aDocShell)->GetHttpChannel(aChannel, getter_AddRefs(httpChannel));
     if (NS_FAILED(rv)) {
       return false;
     }
   }
 
   if (!httpChannel) {
     return true;
   }
@@ -438,21 +476,21 @@ nsDSURIContentListener::CheckFrameOption
     return true;
   }
 
   // iterate through all the header values (usually there's only one, but can
   // be many.  If any want to deny the load, deny the load.
   nsCharSeparatedTokenizer tokenizer(xfoHeaderValue, ',');
   while (tokenizer.hasMoreTokens()) {
     const nsSubstring& tok = tokenizer.nextToken();
-    if (!CheckOneFrameOptionsPolicy(httpChannel, tok)) {
+    if (!CheckOneFrameOptionsPolicy(httpChannel, tok, aDocShell)) {
       // cancel the load and display about:blank
       httpChannel->Cancel(NS_BINDING_ABORTED);
-      if (mDocShell) {
-        nsCOMPtr<nsIWebNavigation> webNav(do_QueryObject(mDocShell));
+      if (aDocShell) {
+        nsCOMPtr<nsIWebNavigation> webNav(do_QueryObject(aDocShell));
         if (webNav) {
           nsCOMPtr<nsILoadInfo> loadInfo = httpChannel->GetLoadInfo();
           nsCOMPtr<nsIPrincipal> triggeringPrincipal = loadInfo
             ? loadInfo->TriggeringPrincipal()
             : nsContentUtils::GetSystemPrincipal();
           webNav->LoadURI(u"about:blank",
                           0, nullptr, nullptr, nullptr,
                           triggeringPrincipal);
@@ -460,17 +498,17 @@ nsDSURIContentListener::CheckFrameOption
       }
       return false;
     }
   }
 
   return true;
 }
 
-void
+/* static */ void
 nsDSURIContentListener::ReportXFOViolation(nsIDocShellTreeItem* aTopDocShellItem,
                                            nsIURI* aThisURI,
                                            XFOHeader aHeader)
 {
   MOZ_ASSERT(aTopDocShellItem, "Need a top docshell");
 
   nsCOMPtr<nsPIDOMWindowOuter> topOuterWindow = aTopDocShellItem->GetWindow();
   if (!topOuterWindow) {
--- a/docshell/base/nsDSURIContentListener.h
+++ b/docshell/base/nsDSURIContentListener.h
@@ -23,43 +23,46 @@ class nsDSURIContentListener final
   friend class nsDocShell;
 
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIURICONTENTLISTENER
 
   nsresult Init();
 
+  // Determine if X-Frame-Options allows content to be framed
+  // as a subdocument
+  static bool CheckFrameOptions(nsIChannel* aChannel,
+                                nsIDocShell* aDocShell,
+                                nsIPrincipal* aPrincipal);
+
 protected:
   explicit nsDSURIContentListener(nsDocShell* aDocShell);
   virtual ~nsDSURIContentListener();
 
   void DropDocShellReference()
   {
     mDocShell = nullptr;
     mExistingJPEGRequest = nullptr;
     mExistingJPEGStreamListener = nullptr;
   }
 
-  // Determine if X-Frame-Options allows content to be framed
-  // as a subdocument
-  bool CheckFrameOptions(nsIRequest* aRequest);
-  bool CheckOneFrameOptionsPolicy(nsIHttpChannel* aHttpChannel,
-                                  const nsAString& aPolicy);
-
+  static bool CheckOneFrameOptionsPolicy(nsIHttpChannel* aHttpChannel,
+                                         const nsAString& aPolicy,
+                                         nsIDocShell* aDocShell);
   enum XFOHeader
   {
     eDENY,
     eSAMEORIGIN,
     eALLOWFROM
   };
 
-  void ReportXFOViolation(nsIDocShellTreeItem* aTopDocShellItem,
-                          nsIURI* aThisURI,
-                          XFOHeader aHeader);
+  static void ReportXFOViolation(nsIDocShellTreeItem* aTopDocShellItem,
+                                 nsIURI* aThisURI,
+                                 XFOHeader aHeader);
 
 protected:
   nsDocShell* mDocShell;
   // Hack to handle multipart images without creating a new viewer
   nsCOMPtr<nsIStreamListener> mExistingJPEGStreamListener;
   nsCOMPtr<nsIChannel> mExistingJPEGRequest;
 
   // Store the parent listener in either of these depending on
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -60,16 +60,17 @@
 #include "mozilla/dom/Attr.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "nsIDOMDOMImplementation.h"
 #include "nsIDOMDocumentXBL.h"
 #include "mozilla/dom/Element.h"
 #include "nsGenericHTMLElement.h"
 #include "mozilla/dom/CDATASection.h"
 #include "mozilla/dom/ProcessingInstruction.h"
+#include "nsDSURIContentListener.h"
 #include "nsDOMString.h"
 #include "nsNodeUtils.h"
 #include "nsLayoutUtils.h" // for GetFrameForPoint
 #include "nsIFrame.h"
 #include "nsITabChild.h"
 
 #include "nsRange.h"
 #include "nsIDOMText.h"
@@ -2579,16 +2580,25 @@ nsDocument::StartDocumentLoad(const char
   }
 
   // If this is not a data document, set CSP.
   if (!mLoadedAsData) {
     nsresult rv = InitCSP(aChannel);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
+  // XFO needs to be checked after CSP because it is ignored if
+  // the CSP defines frame-ancestors.
+  if (!nsDSURIContentListener::CheckFrameOptions(aChannel, docShell, NodePrincipal())) {
+    MOZ_LOG(gCspPRLog, LogLevel::Debug,
+            ("XFO doesn't like frame's ancestry, not loading."));
+    // stop!  ERROR page!
+    aChannel->Cancel(NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION);
+  }
+
   return NS_OK;
 }
 
 void
 nsDocument::SendToConsole(nsCOMArray<nsISecurityConsoleMessage>& aMessages)
 {
   for (uint32_t i = 0; i < aMessages.Length(); ++i) {
     nsAutoString messageTag;
--- a/dom/base/nsDocument.h
+++ b/dom/base/nsDocument.h
@@ -1559,17 +1559,16 @@ private:
   // Recomputes the visibility state but doesn't set the new value.
   mozilla::dom::VisibilityState GetVisibilityState() const;
   void NotifyStyleSheetAdded(mozilla::StyleSheet* aSheet, bool aDocumentSheet);
   void NotifyStyleSheetRemoved(mozilla::StyleSheet* aSheet, bool aDocumentSheet);
 
   void PostUnblockOnloadEvent();
   void DoUnblockOnload();
 
-  nsresult CheckFrameOptions();
   nsresult InitCSP(nsIChannel* aChannel);
 
   /**
    * Find the (non-anonymous) content in this document for aFrame. It will
    * be aFrame's content node if that content is in this document and not
    * anonymous. Otherwise, when aFrame is in a subdocument, we use the frame
    * element containing the subdocument containing aFrame, and/or find the
    * nearest non-anonymous ancestor in this document.
--- a/dom/interfaces/security/nsIContentSecurityPolicy.idl
+++ b/dom/interfaces/security/nsIContentSecurityPolicy.idl
@@ -94,16 +94,21 @@ interface nsIContentSecurityPolicy : nsI
    * Please note that block-all-mixed-content takes presedence in case the
    * directive upgrade-insecure-requests is defined in the same policy and
    * will therefore block all mixed content without even trying to perform
    * an upgrade.
    */
   readonly attribute bool blockAllMixedContent;
 
   /**
+   * Returns whether this policy enforces the frame-ancestors directive.
+   */
+  readonly attribute bool enforcesFrameAncestors;
+
+  /**
    * Obtains the referrer policy (as integer) for this browsing context as
    * specified in CSP.  If there are multiple policies and...
    *  - only one sets a referrer policy: that policy is returned
    *  - more than one sets different referrer policies: no-referrer is returned
    *  - more than one set equivalent policies: that policy is returned
    * For the enumeration of policies see ReferrerPolicy.h and nsIHttpChannel.
    *
    * @param aPolicy
--- a/dom/locales/en-US/chrome/security/csp.properties
+++ b/dom/locales/en-US/chrome/security/csp.properties
@@ -86,16 +86,20 @@ blockAllMixedContent = Blocking insecure request ‘%1$S’.
 # %1$S is the name of a CSP directive that requires additional values (e.g., 'require-sri-for')
 ignoringDirectiveWithNoValues = Ignoring ‘%1$S’ since it does not contain any parameters.
 # LOCALIZATION NOTE (ignoringReportOnlyDirective):
 # %1$S is the directive that is ignored in report-only mode.
 ignoringReportOnlyDirective = Ignoring sandbox directive when delivered in a report-only policy ‘%1$S’
 # LOCALIZATION NOTE (deprecatedReferrerDirective):
 # %1$S is the value of the deprecated Referrer Directive.
 deprecatedReferrerDirective = Referrer Directive ‘%1$S’ has been deprecated. Please use the Referrer-Policy header instead.
+# LOCALIZATION NOTE (IgnoringSrcBecauseOfDirective):
+# %1$S is the name of the src that is ignored.
+# %2$S is the name of the directive that causes the src to be ignored.
+IgnoringSrcBecauseOfDirective=Ignoring ‘%1$S’ because of ‘%2$S’ directive.
 
 # CSP Errors:
 # LOCALIZATION NOTE (couldntParseInvalidSource):
 # %1$S is the source that could not be parsed
 couldntParseInvalidSource = Couldn’t parse invalid source %1$S
 # LOCALIZATION NOTE (couldntParseInvalidHost):
 # %1$S is the host that's invalid
 couldntParseInvalidHost = Couldn’t parse invalid host %1$S
--- a/dom/security/nsCSPContext.cpp
+++ b/dom/security/nsCSPContext.cpp
@@ -345,16 +345,30 @@ nsCSPContext::GetBlockAllMixedContent(bo
       *outBlockAllMixedContent = true;
       return NS_OK;
     }
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsCSPContext::GetEnforcesFrameAncestors(bool *outEnforcesFrameAncestors)
+{
+  *outEnforcesFrameAncestors = false;
+  for (uint32_t i = 0; i < mPolicies.Length(); i++) {
+    if (!mPolicies[i]->getReportOnlyFlag() &&
+        mPolicies[i]->hasDirective(nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE)) {
+      *outEnforcesFrameAncestors = true;
+      return NS_OK;
+    }
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsCSPContext::GetReferrerPolicy(uint32_t* outPolicy, bool* outIsSet)
 {
   *outIsSet = false;
   *outPolicy = mozilla::net::RP_Unset;
   nsAutoString refpol;
   mozilla::net::ReferrerPolicy previousPolicy = mozilla::net::RP_Unset;
   for (uint32_t i = 0; i < mPolicies.Length(); i++) {
     mPolicies[i]->getReferrerPolicy(refpol);