diff --git a/AUTHORS b/AUTHORS index 2c8a12a9fb..f7be63c128 100644 --- a/AUTHORS +++ b/AUTHORS @@ -364,6 +364,7 @@ Florian Scholz France Telecom Research and Development Franck +Francois Marier Frank Tang Frank Yan Franky Braem diff --git a/dom/base/moz.build b/dom/base/moz.build index a2c5a1efe3..dbab2522b5 100644 --- a/dom/base/moz.build +++ b/dom/base/moz.build @@ -429,6 +429,7 @@ LOCAL_INCLUDES += [ '/layout/svg', '/layout/xul', '/netwerk/base', + '/security/manager/ssl', '/widget', '/xpcom/ds', ] diff --git a/dom/base/nsContentSink.cpp b/dom/base/nsContentSink.cpp index c9b38c6ac1..66e9d41960 100644 --- a/dom/base/nsContentSink.cpp +++ b/dom/base/nsContentSink.cpp @@ -51,6 +51,16 @@ #include "nsParserConstants.h" #include "nsSandboxFlags.h" +static PRLogModuleInfo* +GetSriLog() +{ + static PRLogModuleInfo *gSriPRLog; + if (!gSriPRLog) { + gSriPRLog = PR_NewLogModule("SRI"); + } + return gSriPRLog; +} + using namespace mozilla; PRLogModuleInfo* gContentSinkLogModuleInfo; @@ -755,12 +765,23 @@ nsContentSink::ProcessStyleLink(nsIContent* aElement, aElement->NodeType() == nsIDOMNode::PROCESSING_INSTRUCTION_NODE, "We only expect processing instructions here"); + nsAutoString integrity; + if (aElement) { + aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::integrity, integrity); + } + if (!integrity.IsEmpty()) { + MOZ_LOG(GetSriLog(), mozilla::LogLevel::Debug, + ("nsContentSink::ProcessStyleLink, integrity=%s", + NS_ConvertUTF16toUTF8(integrity).get())); + } + // If this is a fragment parser, we don't want to observe. // We don't support CORS for processing instructions bool isAlternate; rv = mCSSLoader->LoadStyleLink(aElement, url, aTitle, aMedia, aAlternate, CORS_NONE, mDocument->GetReferrerPolicy(), - mRunsToCompletion ? nullptr : this, &isAlternate); + integrity, mRunsToCompletion ? nullptr : this, + &isAlternate); NS_ENSURE_SUCCESS(rv, rv); if (!isAlternate && !mRunsToCompletion) { diff --git a/dom/base/nsDocument.cpp b/dom/base/nsDocument.cpp index 469ffd6009..ebd14e4fc1 100644 --- a/dom/base/nsDocument.cpp +++ b/dom/base/nsDocument.cpp @@ -9912,7 +9912,8 @@ NS_IMPL_ISUPPORTS(StubCSSLoaderObserver, nsICSSLoaderObserver) void nsDocument::PreloadStyle(nsIURI* uri, const nsAString& charset, const nsAString& aCrossOriginAttr, - const ReferrerPolicy aReferrerPolicy) + const ReferrerPolicy aReferrerPolicy, + const nsAString& aIntegrity) { // The CSSLoader will retain this object after we return. nsCOMPtr obs = new StubCSSLoaderObserver(); @@ -9922,7 +9923,7 @@ nsDocument::PreloadStyle(nsIURI* uri, const nsAString& charset, NS_LossyConvertUTF16toASCII(charset), obs, Element::StringToCORSMode(aCrossOriginAttr), - aReferrerPolicy); + aReferrerPolicy, aIntegrity); } nsresult diff --git a/dom/base/nsDocument.h b/dom/base/nsDocument.h index cfd02fcc58..275fc0ac00 100644 --- a/dom/base/nsDocument.h +++ b/dom/base/nsDocument.h @@ -1126,7 +1126,8 @@ public: virtual void PreloadStyle(nsIURI* uri, const nsAString& charset, const nsAString& aCrossOriginAttr, - ReferrerPolicy aReferrerPolicy) override; + ReferrerPolicy aReferrerPolicy, + const nsAString& aIntegrity) override; virtual nsresult LoadChromeSheetSync(nsIURI* uri, bool isAgentSheet, mozilla::CSSStyleSheet** sheet) override; diff --git a/dom/base/nsGkAtomList.h b/dom/base/nsGkAtomList.h index c3d6b1fc89..d089cb01ad 100644 --- a/dom/base/nsGkAtomList.h +++ b/dom/base/nsGkAtomList.h @@ -488,6 +488,7 @@ GK_ATOM(instanceOf, "instanceOf") GK_ATOM(int32, "int32") GK_ATOM(int64, "int64") GK_ATOM(integer, "integer") +GK_ATOM(integrity, "integrity") GK_ATOM(intersection, "intersection") GK_ATOM(is, "is") GK_ATOM(iscontainer, "iscontainer") diff --git a/dom/base/nsIDocument.h b/dom/base/nsIDocument.h index c1ded381e4..fe70b7aae5 100644 --- a/dom/base/nsIDocument.h +++ b/dom/base/nsIDocument.h @@ -158,8 +158,8 @@ struct FullScreenOptions { } // namespace mozilla #define NS_IDOCUMENT_IID \ -{ 0x21bbd52a, 0xc2d2, 0x4b2f, \ - { 0xbc, 0x6c, 0xc9, 0x52, 0xbe, 0x23, 0x6b, 0x19 } } +{ 0x6d18ec0b, 0x1f68, 0x4ae6, \ + { 0x8b, 0x3d, 0x8d, 0x7d, 0x8b, 0x8e, 0x28, 0xd4 } } // Enum for requesting a particular type of document when creating a doc enum DocumentFlavor { @@ -2065,7 +2065,8 @@ public: */ virtual void PreloadStyle(nsIURI* aURI, const nsAString& aCharset, const nsAString& aCrossOriginAttr, - ReferrerPolicyEnum aReferrerPolicy) = 0; + ReferrerPolicyEnum aReferrerPolicy, + const nsAString& aIntegrity) = 0; /** * Called by the chrome registry to load style sheets. Can be put diff --git a/dom/base/nsScriptLoader.cpp b/dom/base/nsScriptLoader.cpp index 767644d651..8b33877e54 100644 --- a/dom/base/nsScriptLoader.cpp +++ b/dom/base/nsScriptLoader.cpp @@ -53,9 +53,21 @@ #include "mozilla/Attributes.h" #include "mozilla/unused.h" +#include "mozilla/dom/SRICheck.h" +#include "nsIScriptError.h" static PRLogModuleInfo* gCspPRLog; +static PRLogModuleInfo* +GetSriLog() +{ + static PRLogModuleInfo *gSriPRLog; + if (!gSriPRLog) { + gSriPRLog = PR_NewLogModule("SRI"); + } + return gSriPRLog; +} + using namespace mozilla; using namespace mozilla::dom; @@ -626,7 +638,22 @@ nsScriptLoader::ProcessScriptElement(nsIScriptElement *aElement) if (!request) { // no usable preload - request = new nsScriptLoadRequest(aElement, version, ourCORSMode); + + SRIMetadata sriMetadata; + { + nsAutoString integrity; + scriptContent->GetAttr(kNameSpaceID_None, nsGkAtoms::integrity, + integrity); + if (!integrity.IsEmpty()) { + MOZ_LOG(GetSriLog(), mozilla::LogLevel::Debug, + ("nsScriptLoader::ProcessScriptElement, integrity=%s", + NS_ConvertUTF16toUTF8(integrity).get())); + SRICheck::IntegrityMetadata(integrity, mDocument, &sriMetadata); + } + } + + request = new nsScriptLoadRequest(aElement, version, ourCORSMode, + sriMetadata); request->mURI = scriptURI; request->mIsInline = false; request->mLoading = true; @@ -740,7 +767,8 @@ nsScriptLoader::ProcessScriptElement(nsIScriptElement *aElement) } // Inline scripts ignore ther CORS mode and are always CORS_NONE - request = new nsScriptLoadRequest(aElement, version, CORS_NONE); + request = new nsScriptLoadRequest(aElement, version, CORS_NONE, + SRIMetadata()); // SRI doesn't apply request->mJSVersion = version; request->mLoading = false; request->mIsInline = true; @@ -1436,8 +1464,14 @@ nsScriptLoader::OnStreamComplete(nsIStreamLoader* aLoader, NS_ASSERTION(request, "null request in stream complete handler"); NS_ENSURE_TRUE(request, NS_ERROR_FAILURE); - nsresult rv = PrepareLoadedRequest(request, aLoader, aStatus, aStringLen, - aString); + nsresult rv = NS_ERROR_SRI_CORRUPT; + if (request->mIntegrity.IsEmpty() || + NS_SUCCEEDED(SRICheck::VerifyIntegrity(request->mIntegrity, aLoader, + request->mCORSMode, aStringLen, + aString, mDocument))) { + rv = PrepareLoadedRequest(request, aLoader, aStatus, aStringLen, aString); + } + if (NS_FAILED(rv)) { /* * Handle script not loading error because source was a tracking URL. @@ -1634,6 +1668,7 @@ void nsScriptLoader::PreloadURI(nsIURI *aURI, const nsAString &aCharset, const nsAString &aType, const nsAString &aCrossOrigin, + const nsAString& aIntegrity, bool aScriptFromHead, const mozilla::net::ReferrerPolicy aReferrerPolicy) { @@ -1642,9 +1677,18 @@ nsScriptLoader::PreloadURI(nsIURI *aURI, const nsAString &aCharset, return; } + SRIMetadata sriMetadata; + if (!aIntegrity.IsEmpty()) { + MOZ_LOG(GetSriLog(), mozilla::LogLevel::Debug, + ("nsScriptLoader::PreloadURI, integrity=%s", + NS_ConvertUTF16toUTF8(aIntegrity).get())); + SRICheck::IntegrityMetadata(aIntegrity, mDocument, &sriMetadata); + } + nsRefPtr request = new nsScriptLoadRequest(nullptr, 0, - Element::StringToCORSMode(aCrossOrigin)); + Element::StringToCORSMode(aCrossOrigin), + sriMetadata); request->mURI = aURI; request->mIsInline = false; request->mLoading = true; diff --git a/dom/base/nsScriptLoader.h b/dom/base/nsScriptLoader.h index cb59a87f74..06d323b113 100644 --- a/dom/base/nsScriptLoader.h +++ b/dom/base/nsScriptLoader.h @@ -19,6 +19,7 @@ #include "nsIDocument.h" #include "nsIStreamLoader.h" #include "mozilla/CORSMode.h" +#include "mozilla/dom/SRIMetadata.h" #include "mozilla/LinkedList.h" #include "mozilla/net/ReferrerPolicy.h" @@ -56,7 +57,8 @@ class nsScriptLoadRequest final : public nsISupports, public: nsScriptLoadRequest(nsIScriptElement* aElement, uint32_t aVersion, - mozilla::CORSMode aCORSMode) + mozilla::CORSMode aCORSMode, + const mozilla::dom::SRIMetadata &aIntegrity) : mElement(aElement), mLoading(true), mIsInline(true), @@ -72,6 +74,7 @@ public: mJSVersion(aVersion), mLineNo(1), mCORSMode(aCORSMode), + mIntegrity(aIntegrity), mReferrerPolicy(mozilla::net::RP_Default) { } @@ -129,6 +132,7 @@ public: nsAutoCString mURL; // Keep the URI's filename alive during off thread parsing. int32_t mLineNo; const mozilla::CORSMode mCORSMode; + const mozilla::dom::SRIMetadata mIntegrity; mozilla::net::ReferrerPolicy mReferrerPolicy; }; @@ -374,11 +378,13 @@ public: * @param aType The type parameter for the script. * @param aCrossOrigin The crossorigin attribute for the script. * Void if not present. + * @param aIntegrity The expect hash url, if avail, of the request * @param aScriptFromHead Whether or not the script was a child of head */ virtual void PreloadURI(nsIURI *aURI, const nsAString &aCharset, const nsAString &aType, const nsAString &aCrossOrigin, + const nsAString& aIntegrity, bool aScriptFromHead, const mozilla::net::ReferrerPolicy aReferrerPolicy); diff --git a/dom/base/nsStyleLinkElement.cpp b/dom/base/nsStyleLinkElement.cpp index f8992a0fef..1d2f7d383b 100644 --- a/dom/base/nsStyleLinkElement.cpp +++ b/dom/base/nsStyleLinkElement.cpp @@ -421,13 +421,21 @@ nsStyleLinkElement::DoUpdateStyleSheet(nsIDocument* aOldDocument, scopeElement, aObserver, &doneLoading, &isAlternate); } else { + nsAutoString integrity; + thisContent->GetAttr(kNameSpaceID_None, nsGkAtoms::integrity, integrity); + if (!integrity.IsEmpty()) { + MOZ_LOG(GetSriLog(), mozilla::LogLevel::Debug, + ("nsStyleLinkElement::DoUpdateStyleSheet, integrity=%s", + NS_ConvertUTF16toUTF8(integrity).get())); + } + // XXXbz clone the URI here to work around content policies modifying URIs. nsCOMPtr clonedURI; uri->Clone(getter_AddRefs(clonedURI)); NS_ENSURE_TRUE(clonedURI, NS_ERROR_OUT_OF_MEMORY); rv = doc->CSSLoader()-> LoadStyleLink(thisContent, clonedURI, title, media, isAlternate, - GetCORSMode(), doc->GetReferrerPolicy(), + GetCORSMode(), doc->GetReferrerPolicy(), integrity, aObserver, &isAlternate); if (NS_FAILED(rv)) { // Don't propagate LoadStyleLink() errors further than this, since some diff --git a/dom/base/test/file_bug1091883_frame.html b/dom/base/test/file_bug1091883_frame.html new file mode 100644 index 0000000000..a245175ae3 --- /dev/null +++ b/dom/base/test/file_bug1091883_frame.html @@ -0,0 +1,13 @@ + + + +

Frame I am.

+ + + diff --git a/dom/base/test/file_bug1091883_subframe.html b/dom/base/test/file_bug1091883_subframe.html new file mode 100644 index 0000000000..f282e1eeb9 --- /dev/null +++ b/dom/base/test/file_bug1091883_subframe.html @@ -0,0 +1,6 @@ + + + +

Subframe I am.

+ + diff --git a/dom/base/test/file_bug1091883_target.html b/dom/base/test/file_bug1091883_target.html new file mode 100644 index 0000000000..2095595fbc --- /dev/null +++ b/dom/base/test/file_bug1091883_target.html @@ -0,0 +1,13 @@ + + + +

Target I am.

+ + + + + diff --git a/dom/base/test/mochitest.ini b/dom/base/test/mochitest.ini index 3be17554f8..43e98a7007 100644 --- a/dom/base/test/mochitest.ini +++ b/dom/base/test/mochitest.ini @@ -92,6 +92,9 @@ support-files = file_XHR_system_redirect.html^headers^ file_XHR_timeout.sjs file_base_xbl.xml + file_bug1091883_frame.html + file_bug1091883_subframe.html + file_bug1091883_target.html file_bug28293.sjs file_bug326337.xml file_bug326337_inner.html @@ -247,6 +250,7 @@ support-files = [test_audioWindowUtils.html] [test_audioNotification.html] skip-if = buildapp == 'mulet' +[test_bug1091883.html] [test_bug116083.html] [test_bug793311.html] [test_bug913761.html] diff --git a/dom/base/test/test_bug1091883.html b/dom/base/test/test_bug1091883.html new file mode 100644 index 0000000000..2ec4cf1aed --- /dev/null +++ b/dom/base/test/test_bug1091883.html @@ -0,0 +1,89 @@ + + + + + + + Test for Bug 1091883 + + + + +

Mozilla Bug 1091883

+

Results

+
Running...
+ + + + + diff --git a/dom/html/HTMLLinkElement.cpp b/dom/html/HTMLLinkElement.cpp index 80856beb61..bead7f5c43 100644 --- a/dom/html/HTMLLinkElement.cpp +++ b/dom/html/HTMLLinkElement.cpp @@ -214,6 +214,11 @@ HTMLLinkElement::ParseAttribute(int32_t aNamespaceID, aResult.ParseAtomArray(aValue); return true; } + + if (aAttribute == nsGkAtoms::integrity) { + aResult.ParseStringOrAtom(aValue); + return true; + } } return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, diff --git a/dom/html/HTMLLinkElement.h b/dom/html/HTMLLinkElement.h index 22825dfe70..a35f4ac42e 100644 --- a/dom/html/HTMLLinkElement.h +++ b/dom/html/HTMLLinkElement.h @@ -136,6 +136,14 @@ public: { SetHTMLAttr(nsGkAtoms::target, aTarget, aRv); } + void GetIntegrity(nsAString& aIntegrity) const + { + GetHTMLAttr(nsGkAtoms::integrity, aIntegrity); + } + void SetIntegrity(const nsAString& aIntegrity, ErrorResult& aRv) + { + SetHTMLAttr(nsGkAtoms::integrity, aIntegrity, aRv); + } already_AddRefed GetImport(); already_AddRefed GetImportLoader() diff --git a/dom/html/HTMLScriptElement.cpp b/dom/html/HTMLScriptElement.cpp index aa0837252b..7c1cd7d702 100644 --- a/dom/html/HTMLScriptElement.cpp +++ b/dom/html/HTMLScriptElement.cpp @@ -75,10 +75,16 @@ HTMLScriptElement::ParseAttribute(int32_t aNamespaceID, const nsAString& aValue, nsAttrValue& aResult) { - if (aNamespaceID == kNameSpaceID_None && - aAttribute == nsGkAtoms::crossorigin) { - ParseCORSValue(aValue, aResult); - return true; + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::crossorigin) { + ParseCORSValue(aValue, aResult); + return true; + } + + if (aAttribute == nsGkAtoms::integrity) { + aResult.ParseStringOrAtom(aValue); + return true; + } } return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, diff --git a/dom/html/HTMLScriptElement.h b/dom/html/HTMLScriptElement.h index 5de8785a99..f12866bd0a 100644 --- a/dom/html/HTMLScriptElement.h +++ b/dom/html/HTMLScriptElement.h @@ -79,6 +79,14 @@ public: { SetOrRemoveNullableStringAttr(nsGkAtoms::crossorigin, aCrossOrigin, aError); } + void GetIntegrity(nsAString& aIntegrity) + { + GetHTMLAttr(nsGkAtoms::integrity, aIntegrity); + } + void SetIntegrity(const nsAString& aIntegrity, ErrorResult& rv) + { + SetHTMLAttr(nsGkAtoms::integrity, aIntegrity, rv); + } bool Async(); void SetAsync(bool aValue, ErrorResult& rv); diff --git a/dom/locales/en-US/chrome/security/security.properties b/dom/locales/en-US/chrome/security/security.properties index f67347d870..b47b1f735f 100644 --- a/dom/locales/en-US/chrome/security/security.properties +++ b/dom/locales/en-US/chrome/security/security.properties @@ -32,6 +32,22 @@ LoadingMixedDisplayContent2=Loading mixed (insecure) display content "%1$S" on a # LOCALIZATION NOTE: Do not translate "allow-scripts", "allow-same-origin", "sandbox" or "iframe" BothAllowScriptsAndSameOriginPresent=An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can remove its sandboxing. +# Sub-Resource Integrity +# LOCALIZATION NOTE: Do not translate "script" or "integrity". "%1$S" is the invalid token found in the attribute. +MalformedIntegrityHash=The script element has a malformed hash in its integrity attribute: "%1$S". The correct format is "-". +# LOCALIZATION NOTE: Do not translate "integrity" +InvalidIntegrityLength=The hash contained in the integrity attribute has the wrong length. +# LOCALIZATION NOTE: Do not translate "integrity" +InvalidIntegrityBase64=The hash contained in the integrity attribute could not be decoded. +# LOCALIZATION NOTE: Do not translate "integrity". "%1$S" is the type of hash algorigthm in use (e.g. "sha256"). +IntegrityMismatch=None of the "%1$S" hashes in the integrity attribute match the content of the subresource. +# LOCALIZATION NOTE: "%1$S" is the URI of the sub-resource that cannot be protected using SRI. +IneligibleResource="%1$S" is not eligible for integrity checks since it's neither CORS-enabled nor same-origin. +# LOCALIZATION NOTE: Do not translate "integrity". "%1$S" is the invalid hash algorithm found in the attribute. +UnsupportedHashAlg=Unsupported hash algorithm in the integrity attribute: "%1$S" +# LOCALIZATION NOTE: Do not translate "integrity" +NoValidMetadata=The integrity attribute does not contain any valid metadata. + # LOCALIZATION NOTE: Do not translate "SSL 3.0". WeakProtocolVersionWarning=This site uses the protocol SSL 3.0 for encryption, which is deprecated and insecure. # LOCALIZATION NOTE: Do not translate "RC4". diff --git a/dom/security/SRICheck.cpp b/dom/security/SRICheck.cpp new file mode 100644 index 0000000000..45797e840b --- /dev/null +++ b/dom/security/SRICheck.cpp @@ -0,0 +1,349 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "SRICheck.h" + +#include "mozilla/Base64.h" +#include "mozilla/Logging.h" +#include "mozilla/Preferences.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsICryptoHash.h" +#include "nsIDocument.h" +#include "nsIProtocolHandler.h" +#include "nsIScriptError.h" +#include "nsIScriptSecurityManager.h" +#include "nsIStreamLoader.h" +#include "nsIUnicharStreamLoader.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsWhitespaceTokenizer.h" + +static PRLogModuleInfo* +GetSriLog() +{ + static PRLogModuleInfo *gSriPRLog; + if (!gSriPRLog) { + gSriPRLog = PR_NewLogModule("SRI"); + } + return gSriPRLog; +} + +#define SRILOG(args) MOZ_LOG(GetSriLog(), mozilla::LogLevel::Debug, args) +#define SRIERROR(args) MOZ_LOG(GetSriLog(), mozilla::LogLevel::Error, args) + +namespace mozilla { +namespace dom { + +/** + * Returns whether or not the sub-resource about to be loaded is eligible + * for integrity checks. If it's not, the checks will be skipped and the + * sub-resource will be loaded. + */ +static nsresult +IsEligible(nsIChannel* aChannel, const CORSMode aCORSMode, + const nsIDocument* aDocument) +{ + NS_ENSURE_ARG_POINTER(aDocument); + + if (!aChannel) { + SRILOG(("SRICheck::IsEligible, null channel")); + return NS_ERROR_SRI_NOT_ELIGIBLE; + } + + // Was the sub-resource loaded via CORS? + if (aCORSMode != CORS_NONE) { + SRILOG(("SRICheck::IsEligible, CORS mode")); + return NS_OK; + } + + nsCOMPtr finalURI; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr originalURI; + rv = aChannel->GetOriginalURI(getter_AddRefs(originalURI)); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString requestSpec; + rv = originalURI->GetSpec(requestSpec); + NS_ENSURE_SUCCESS(rv, rv); + + if (MOZ_LOG_TEST(GetSriLog(), mozilla::LogLevel::Debug)) { + nsAutoCString documentSpec, finalSpec; + aDocument->GetDocumentURI()->GetAsciiSpec(documentSpec); + if (finalURI) { + finalURI->GetSpec(finalSpec); + } + SRILOG(("SRICheck::IsEligible, documentURI=%s; requestURI=%s; finalURI=%s", + documentSpec.get(), requestSpec.get(), finalSpec.get())); + } + + // Is the sub-resource same-origin? + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + if (NS_SUCCEEDED(ssm->CheckSameOriginURI(aDocument->GetDocumentURI(), + finalURI, false))) { + SRILOG(("SRICheck::IsEligible, same-origin")); + return NS_OK; + } + SRILOG(("SRICheck::IsEligible, NOT same origin")); + + NS_ConvertUTF8toUTF16 requestSpecUTF16(requestSpec); + const char16_t* params[] = { requestSpecUTF16.get() }; + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, + NS_LITERAL_CSTRING("Sub-resource Integrity"), + aDocument, + nsContentUtils::eSECURITY_PROPERTIES, + "IneligibleResource", + params, ArrayLength(params)); + return NS_ERROR_SRI_NOT_ELIGIBLE; +} + +/** + * Compute the hash of a sub-resource and compare it with the expected + * value. + */ +static nsresult +VerifyHash(const SRIMetadata& aMetadata, uint32_t aHashIndex, + uint32_t aStringLen, const uint8_t* aString, + const nsIDocument* aDocument) +{ + NS_ENSURE_ARG_POINTER(aString); + NS_ENSURE_ARG_POINTER(aDocument); + + nsAutoCString base64Hash; + aMetadata.GetHash(aHashIndex, &base64Hash); + SRILOG(("SRICheck::VerifyHash, hash[%u]=%s", aHashIndex, base64Hash.get())); + + nsAutoCString binaryHash; + if (NS_WARN_IF(NS_FAILED(Base64Decode(base64Hash, binaryHash)))) { + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, + NS_LITERAL_CSTRING("Sub-resource Integrity"), + aDocument, + nsContentUtils::eSECURITY_PROPERTIES, + "InvalidIntegrityBase64"); + return NS_ERROR_SRI_CORRUPT; + } + + uint32_t hashLength; + int8_t hashType; + aMetadata.GetHashType(&hashType, &hashLength); + if (binaryHash.Length() != hashLength) { + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, + NS_LITERAL_CSTRING("Sub-resource Integrity"), + aDocument, + nsContentUtils::eSECURITY_PROPERTIES, + "InvalidIntegrityLength"); + return NS_ERROR_SRI_CORRUPT; + } + + nsresult rv; + nsCOMPtr cryptoHash = + do_CreateInstance("@mozilla.org/security/hash;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = cryptoHash->Init(hashType); + NS_ENSURE_SUCCESS(rv, rv); + rv = cryptoHash->Update(aString, aStringLen); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString computedHash; + rv = cryptoHash->Finish(false, computedHash); + NS_ENSURE_SUCCESS(rv, rv); + if (!binaryHash.Equals(computedHash)) { + SRILOG(("SRICheck::VerifyHash, hash[%u] did not match", aHashIndex)); + return NS_ERROR_SRI_CORRUPT; + } + + SRILOG(("SRICheck::VerifyHash, hash[%u] verified successfully", aHashIndex)); + return NS_OK; +} + +/* static */ nsresult +SRICheck::IntegrityMetadata(const nsAString& aMetadataList, + const nsIDocument* aDocument, + SRIMetadata* outMetadata) +{ + NS_ENSURE_ARG_POINTER(outMetadata); + NS_ENSURE_ARG_POINTER(aDocument); + MOZ_ASSERT(outMetadata->IsEmpty()); // caller must pass empty metadata + + if (!Preferences::GetBool("security.sri.enable", false)) { + SRILOG(("SRICheck::IntegrityMetadata, sri is disabled (pref)")); + return NS_ERROR_SRI_DISABLED; + } + + // put a reasonable bound on the length of the metadata + NS_ConvertUTF16toUTF8 metadataList(aMetadataList); + if (metadataList.Length() > SRICheck::MAX_METADATA_LENGTH) { + metadataList.Truncate(SRICheck::MAX_METADATA_LENGTH); + } + MOZ_ASSERT(metadataList.Length() <= aMetadataList.Length()); + + // the integrity attribute is a list of whitespace-separated hashes + // and options so we need to look at them one by one and pick the + // strongest (valid) one + nsCWhitespaceTokenizer tokenizer(metadataList); + nsAutoCString token; + for (uint32_t i=0; tokenizer.hasMoreTokens() && + i < SRICheck::MAX_METADATA_TOKENS; ++i) { + token = tokenizer.nextToken(); + + SRIMetadata metadata(token); + if (metadata.IsMalformed()) { + NS_ConvertUTF8toUTF16 tokenUTF16(token); + const char16_t* params[] = { tokenUTF16.get() }; + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("Sub-resource Integrity"), + aDocument, + nsContentUtils::eSECURITY_PROPERTIES, + "MalformedIntegrityHash", + params, ArrayLength(params)); + } else if (!metadata.IsAlgorithmSupported()) { + nsAutoCString alg; + metadata.GetAlgorithm(&alg); + NS_ConvertUTF8toUTF16 algUTF16(alg); + const char16_t* params[] = { algUTF16.get() }; + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("Sub-resource Integrity"), + aDocument, + nsContentUtils::eSECURITY_PROPERTIES, + "UnsupportedHashAlg", + params, ArrayLength(params)); + } + + nsAutoCString alg1, alg2; + if (MOZ_LOG_TEST(GetSriLog(), mozilla::LogLevel::Debug)) { + outMetadata->GetAlgorithm(&alg1); + metadata.GetAlgorithm(&alg2); + } + if (*outMetadata == metadata) { + SRILOG(("SRICheck::IntegrityMetadata, alg '%s' is the same as '%s'", + alg1.get(), alg2.get())); + *outMetadata += metadata; // add new hash to strongest metadata + } else if (*outMetadata < metadata) { + SRILOG(("SRICheck::IntegrityMetadata, alg '%s' is weaker than '%s'", + alg1.get(), alg2.get())); + *outMetadata = metadata; // replace strongest metadata with current + } + } + + if (MOZ_LOG_TEST(GetSriLog(), mozilla::LogLevel::Debug)) { + if (outMetadata->IsValid()) { + nsAutoCString alg; + outMetadata->GetAlgorithm(&alg); + SRILOG(("SRICheck::IntegrityMetadata, using a '%s' hash", alg.get())); + } else if (outMetadata->IsEmpty()) { + SRILOG(("SRICheck::IntegrityMetadata, no metadata")); + } else { + SRILOG(("SRICheck::IntegrityMetadata, no valid metadata found")); + } + } + return NS_OK; +} + +static nsresult +VerifyIntegrityInternal(const SRIMetadata& aMetadata, + nsIChannel* aChannel, + const CORSMode aCORSMode, + uint32_t aStringLen, + const uint8_t* aString, + const nsIDocument* aDocument) +{ + MOZ_ASSERT(!aMetadata.IsEmpty()); // should be checked by caller + + // IntegrityMetadata() checks this and returns "no metadata" if + // it's disabled so we should never make it this far + MOZ_ASSERT(Preferences::GetBool("security.sri.enable", false)); + + if (NS_FAILED(IsEligible(aChannel, aCORSMode, aDocument))) { + return NS_ERROR_SRI_NOT_ELIGIBLE; + } + if (!aMetadata.IsValid()) { + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("Sub-resource Integrity"), + aDocument, + nsContentUtils::eSECURITY_PROPERTIES, + "NoValidMetadata"); + return NS_OK; // ignore invalid metadata for forward-compatibility + } + + for (uint32_t i = 0; i < aMetadata.HashCount(); i++) { + if (NS_SUCCEEDED(VerifyHash(aMetadata, i, aStringLen, + aString, aDocument))) { + return NS_OK; // stop at the first valid hash + } + } + + nsAutoCString alg; + aMetadata.GetAlgorithm(&alg); + NS_ConvertUTF8toUTF16 algUTF16(alg); + const char16_t* params[] = { algUTF16.get() }; + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, + NS_LITERAL_CSTRING("Sub-resource Integrity"), + aDocument, + nsContentUtils::eSECURITY_PROPERTIES, + "IntegrityMismatch", + params, ArrayLength(params)); + return NS_ERROR_SRI_CORRUPT; +} + +/* static */ nsresult +SRICheck::VerifyIntegrity(const SRIMetadata& aMetadata, + nsIUnicharStreamLoader* aLoader, + const CORSMode aCORSMode, + const nsAString& aString, + const nsIDocument* aDocument) +{ + NS_ENSURE_ARG_POINTER(aLoader); + + NS_ConvertUTF16toUTF8 utf8Hash(aString); + nsCOMPtr channel; + aLoader->GetChannel(getter_AddRefs(channel)); + + if (MOZ_LOG_TEST(GetSriLog(), mozilla::LogLevel::Debug)) { + nsAutoCString requestURL; + nsCOMPtr originalURI; + if (channel && + NS_SUCCEEDED(channel->GetOriginalURI(getter_AddRefs(originalURI))) && + originalURI) { + originalURI->GetAsciiSpec(requestURL); + } + SRILOG(("SRICheck::VerifyIntegrity (unichar stream), url=%s (length=%u)", + requestURL.get(), utf8Hash.Length())); + } + + return VerifyIntegrityInternal(aMetadata, channel, aCORSMode, + utf8Hash.Length(), (uint8_t*)utf8Hash.get(), + aDocument); +} + +/* static */ nsresult +SRICheck::VerifyIntegrity(const SRIMetadata& aMetadata, + nsIStreamLoader* aLoader, + const CORSMode aCORSMode, + uint32_t aStringLen, + const uint8_t* aString, + const nsIDocument* aDocument) +{ + NS_ENSURE_ARG_POINTER(aLoader); + + nsCOMPtr request; + aLoader->GetRequest(getter_AddRefs(request)); + NS_ENSURE_ARG_POINTER(request); + nsCOMPtr channel; + channel = do_QueryInterface(request); + + if (MOZ_LOG_TEST(GetSriLog(), mozilla::LogLevel::Debug)) { + nsAutoCString requestURL; + request->GetName(requestURL); + SRILOG(("SRICheck::VerifyIntegrity (stream), url=%s (length=%u)", + requestURL.get(), aStringLen)); + } + + return VerifyIntegrityInternal(aMetadata, channel, aCORSMode, + aStringLen, aString, aDocument); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/security/SRICheck.h b/dom/security/SRICheck.h new file mode 100644 index 0000000000..c6ac2e6b0b --- /dev/null +++ b/dom/security/SRICheck.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_SRICheck_h +#define mozilla_dom_SRICheck_h + +#include "mozilla/CORSMode.h" +#include "nsCOMPtr.h" +#include "SRIMetadata.h" + +class nsIDocument; +class nsIStreamLoader; +class nsIUnicharStreamLoader; + +namespace mozilla { +namespace dom { + +class SRICheck final +{ +public: + static const uint32_t MAX_METADATA_LENGTH = 24*1024; + static const uint32_t MAX_METADATA_TOKENS = 512; + + /** + * Parse the multiple hashes specified in the integrity attribute and + * return the strongest supported hash. + */ + static nsresult IntegrityMetadata(const nsAString& aMetadataList, + const nsIDocument* aDocument, + SRIMetadata* outMetadata); + + /** + * Process the integrity attribute of the element. A result of false + * must prevent the resource from loading. + */ + static nsresult VerifyIntegrity(const SRIMetadata& aMetadata, + nsIUnicharStreamLoader* aLoader, + const CORSMode aCORSMode, + const nsAString& aString, + const nsIDocument* aDocument); + + /** + * Process the integrity attribute of the element. A result of false + * must prevent the resource from loading. + */ + static nsresult VerifyIntegrity(const SRIMetadata& aMetadata, + nsIStreamLoader* aLoader, + const CORSMode aCORSMode, + uint32_t aStringLen, + const uint8_t* aString, + const nsIDocument* aDocument); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_SRICheck_h diff --git a/dom/security/SRIMetadata.cpp b/dom/security/SRIMetadata.cpp new file mode 100644 index 0000000000..72b0576918 --- /dev/null +++ b/dom/security/SRIMetadata.cpp @@ -0,0 +1,172 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "SRIMetadata.h" + +#include "hasht.h" +#include "mozilla/dom/URLSearchParams.h" +#include "mozilla/Logging.h" +#include "nsICryptoHash.h" + +static PRLogModuleInfo* +GetSriMetadataLog() +{ + static PRLogModuleInfo *gSriMetadataPRLog; + if (!gSriMetadataPRLog) { + gSriMetadataPRLog = PR_NewLogModule("SRIMetadata"); + } + return gSriMetadataPRLog; +} + +#define SRIMETADATALOG(args) MOZ_LOG(GetSriMetadataLog(), mozilla::LogLevel::Debug, args) +#define SRIMETADATAERROR(args) MOZ_LOG(GetSriMetadataLog(), mozilla::LogLevel::Error, args) + +namespace mozilla { +namespace dom { + +SRIMetadata::SRIMetadata(const nsACString& aToken) + : mAlgorithmType(SRIMetadata::UNKNOWN_ALGORITHM), mEmpty(false) +{ + MOZ_ASSERT(!aToken.IsEmpty()); // callers should check this first + + SRIMETADATALOG(("SRIMetadata::SRIMetadata, aToken='%s'", + PromiseFlatCString(aToken).get())); + + int32_t hyphen = aToken.FindChar('-'); + if (hyphen == -1) { + SRIMETADATAERROR(("SRIMetadata::SRIMetadata, invalid (no hyphen)")); + return; // invalid metadata + } + + // split the token into its components + mAlgorithm = Substring(aToken, 0, hyphen); + uint32_t hashStart = hyphen + 1; + if (hashStart >= aToken.Length()) { + SRIMETADATAERROR(("SRIMetadata::SRIMetadata, invalid (missing digest)")); + return; // invalid metadata + } + int32_t question = aToken.FindChar('?'); + if (question == -1) { + mHashes.AppendElement(Substring(aToken, hashStart, + aToken.Length() - hashStart)); + } else { + MOZ_ASSERT(question > 0); + if (static_cast(question) <= hashStart) { + SRIMETADATAERROR(("SRIMetadata::SRIMetadata, invalid (options w/o digest)")); + return; // invalid metadata + } + mHashes.AppendElement(Substring(aToken, hashStart, + question - hashStart)); + } + + if (mAlgorithm.EqualsLiteral("sha256")) { + mAlgorithmType = nsICryptoHash::SHA256; + } else if (mAlgorithm.EqualsLiteral("sha384")) { + mAlgorithmType = nsICryptoHash::SHA384; + } else if (mAlgorithm.EqualsLiteral("sha512")) { + mAlgorithmType = nsICryptoHash::SHA512; + } + + SRIMETADATALOG(("SRIMetadata::SRIMetadata, hash='%s'; alg='%s'", + mHashes[0].get(), mAlgorithm.get())); +} + +bool +SRIMetadata::operator<(const SRIMetadata& aOther) const +{ + static_assert(nsICryptoHash::SHA256 < nsICryptoHash::SHA384, + "We rely on the order indicating relative alg strength"); + static_assert(nsICryptoHash::SHA384 < nsICryptoHash::SHA512, + "We rely on the order indicating relative alg strength"); + MOZ_ASSERT(mAlgorithmType == SRIMetadata::UNKNOWN_ALGORITHM || + mAlgorithmType == nsICryptoHash::SHA256 || + mAlgorithmType == nsICryptoHash::SHA384 || + mAlgorithmType == nsICryptoHash::SHA512); + MOZ_ASSERT(aOther.mAlgorithmType == SRIMetadata::UNKNOWN_ALGORITHM || + aOther.mAlgorithmType == nsICryptoHash::SHA256 || + aOther.mAlgorithmType == nsICryptoHash::SHA384 || + aOther.mAlgorithmType == nsICryptoHash::SHA512); + + if (mEmpty) { + SRIMETADATALOG(("SRIMetadata::operator<, first metadata is empty")); + return true; // anything beats the empty metadata (incl. invalid ones) + } + + SRIMETADATALOG(("SRIMetadata::operator<, alg1='%d'; alg2='%d'", + mAlgorithmType, aOther.mAlgorithmType)); + return (mAlgorithmType < aOther.mAlgorithmType); +} + +bool +SRIMetadata::operator>(const SRIMetadata& aOther) const +{ + MOZ_ASSERT(false); + return false; +} + +SRIMetadata& +SRIMetadata::operator+=(const SRIMetadata& aOther) +{ + MOZ_ASSERT(!aOther.IsEmpty() && !IsEmpty()); + MOZ_ASSERT(aOther.IsValid() && IsValid()); + MOZ_ASSERT(mAlgorithmType == aOther.mAlgorithmType); + + // We only pull in the first element of the other metadata + MOZ_ASSERT(aOther.mHashes.Length() == 1); + if (mHashes.Length() < SRIMetadata::MAX_ALTERNATE_HASHES) { + SRIMETADATALOG(("SRIMetadata::operator+=, appending another '%s' hash (new length=%d)", + mAlgorithm.get(), mHashes.Length())); + mHashes.AppendElement(aOther.mHashes[0]); + } + + MOZ_ASSERT(mHashes.Length() > 1); + MOZ_ASSERT(mHashes.Length() <= SRIMetadata::MAX_ALTERNATE_HASHES); + return *this; +} + +bool +SRIMetadata::operator==(const SRIMetadata& aOther) const +{ + if (IsEmpty() || !IsValid()) { + return false; + } + return mAlgorithmType == aOther.mAlgorithmType; +} + +void +SRIMetadata::GetHash(uint32_t aIndex, nsCString* outHash) const +{ + MOZ_ASSERT(aIndex < SRIMetadata::MAX_ALTERNATE_HASHES); + if (NS_WARN_IF(aIndex >= mHashes.Length())) { + *outHash = nullptr; + return; + } + *outHash = mHashes[aIndex]; +} + +void +SRIMetadata::GetHashType(int8_t* outType, uint32_t* outLength) const +{ + // these constants are defined in security/nss/lib/util/hasht.h and + // netwerk/base/public/nsICryptoHash.idl + switch (mAlgorithmType) { + case nsICryptoHash::SHA256: + *outLength = SHA256_LENGTH; + break; + case nsICryptoHash::SHA384: + *outLength = SHA384_LENGTH; + break; + case nsICryptoHash::SHA512: + *outLength = SHA512_LENGTH; + break; + default: + *outLength = 0; + } + *outType = mAlgorithmType; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/security/SRIMetadata.h b/dom/security/SRIMetadata.h new file mode 100644 index 0000000000..4a37c646a3 --- /dev/null +++ b/dom/security/SRIMetadata.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_SRIMetadata_h +#define mozilla_dom_SRIMetadata_h + +#include "nsTArray.h" +#include "nsString.h" + +namespace mozilla { +namespace dom { + +class SRIMetadata final +{ +public: + static const uint32_t MAX_ALTERNATE_HASHES = 256; + static const int8_t UNKNOWN_ALGORITHM = -1; + + /** + * Create an empty metadata object. + */ + SRIMetadata() : mAlgorithmType(UNKNOWN_ALGORITHM), mEmpty(true) {} + + /** + * Split a string token into the components of an SRI metadata + * attribute. + */ + explicit SRIMetadata(const nsACString& aToken); + + /** + * Returns true when this object's hash algorithm is weaker than the + * other object's hash algorithm. + */ + bool operator<(const SRIMetadata& aOther) const; + + /** + * Not implemented. Should not be used. + */ + bool operator>(const SRIMetadata& aOther) const; + + /** + * Add another metadata's hash to this one. + */ + SRIMetadata& operator+=(const SRIMetadata& aOther); + + /** + * Returns true when the two metadata use the same hash algorithm. + */ + bool operator==(const SRIMetadata& aOther) const; + + bool IsEmpty() const { return mEmpty; } + bool IsMalformed() const { return mHashes.IsEmpty() || mAlgorithm.IsEmpty(); } + bool IsAlgorithmSupported() const { return mAlgorithmType != UNKNOWN_ALGORITHM; } + bool IsValid() const { return !IsMalformed() && IsAlgorithmSupported(); } + + uint32_t HashCount() const { return mHashes.Length(); } + void GetHash(uint32_t aIndex, nsCString* outHash) const; + void GetAlgorithm(nsCString* outAlg) const { *outAlg = mAlgorithm; } + void GetHashType(int8_t* outType, uint32_t* outLength) const; + +private: + nsTArray mHashes; + nsCString mAlgorithm; + int8_t mAlgorithmType; + bool mEmpty; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_SRIMetadata_h diff --git a/dom/security/moz.build b/dom/security/moz.build index 5b31185583..e5e2bec4b2 100644 --- a/dom/security/moz.build +++ b/dom/security/moz.build @@ -11,6 +11,8 @@ EXPORTS.mozilla.dom += [ 'nsCSPService.h', 'nsCSPUtils.h', 'nsMixedContentBlocker.h', + 'SRICheck.h', + 'SRIMetadata.h', ] EXPORTS += [ @@ -24,6 +26,8 @@ UNIFIED_SOURCES += [ 'nsCSPService.cpp', 'nsCSPUtils.cpp', 'nsMixedContentBlocker.cpp', + 'SRICheck.cpp', + 'SRIMetadata.cpp', ] FAIL_ON_WARNINGS = True diff --git a/dom/security/test/moz.build b/dom/security/test/moz.build index 8eaba35c9c..ad11c90fba 100644 --- a/dom/security/test/moz.build +++ b/dom/security/test/moz.build @@ -16,6 +16,7 @@ MOCHITEST_MANIFESTS += [ 'cors/mochitest.ini', 'csp/mochitest.ini', 'mixedcontentblocker/mochitest.ini', + 'sri/mochitest.ini', ] MOCHITEST_CHROME_MANIFESTS += [ diff --git a/dom/security/test/sri/iframe_script_crossdomain.html b/dom/security/test/sri/iframe_script_crossdomain.html new file mode 100644 index 0000000000..f9da6c6775 --- /dev/null +++ b/dom/security/test/sri/iframe_script_crossdomain.html @@ -0,0 +1,135 @@ + + + + + + + + +

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dom/security/test/sri/iframe_script_sameorigin.html b/dom/security/test/sri/iframe_script_sameorigin.html new file mode 100644 index 0000000000..b891db5475 --- /dev/null +++ b/dom/security/test/sri/iframe_script_sameorigin.html @@ -0,0 +1,249 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ +
+
+ + diff --git a/dom/security/test/sri/iframe_sri_disabled.html b/dom/security/test/sri/iframe_sri_disabled.html new file mode 100644 index 0000000000..9fb10293a9 --- /dev/null +++ b/dom/security/test/sri/iframe_sri_disabled.html @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + +

+ +
+
+ + diff --git a/dom/security/test/sri/iframe_style_crossdomain.html b/dom/security/test/sri/iframe_style_crossdomain.html new file mode 100644 index 0000000000..814c78703d --- /dev/null +++ b/dom/security/test/sri/iframe_style_crossdomain.html @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +

This should be red but + this should remain black.

+

+ +
+
+ + diff --git a/dom/security/test/sri/iframe_style_sameorigin.html b/dom/security/test/sri/iframe_style_sameorigin.html new file mode 100644 index 0000000000..29d013bcbd --- /dev/null +++ b/dom/security/test/sri/iframe_style_sameorigin.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +

This should be red and + this should be blue. + However, this should stay black and + this should also stay black. +

+ +

+ +
+
+ + diff --git a/dom/security/test/sri/mochitest.ini b/dom/security/test/sri/mochitest.ini new file mode 100644 index 0000000000..18bd923310 --- /dev/null +++ b/dom/security/test/sri/mochitest.ini @@ -0,0 +1,36 @@ +[DEFAULT] +support-files = + iframe_script_crossdomain.html + iframe_script_sameorigin.html + iframe_sri_disabled.html + iframe_style_crossdomain.html + iframe_style_sameorigin.html + script_crossdomain1.js + script_crossdomain1.js^headers^ + script_crossdomain2.js + script_crossdomain3.js + script_crossdomain3.js^headers^ + script_crossdomain4.js + script_crossdomain4.js^headers^ + script_crossdomain5.js + script_crossdomain5.js^headers^ + script.js + script.js^headers^ + script_301.js + script_301.js^headers^ + script_302.js + script_302.js^headers^ + script_401.js + script_401.js^headers^ + style1.css + style1.css^headers^ + style2.css + style3.css + style_301.css + style_301.css^headers^ + +[test_script_sameorigin.html] +[test_script_crossdomain.html] +[test_sri_disabled.html] +[test_style_crossdomain.html] +[test_style_sameorigin.html] diff --git a/dom/security/test/sri/script.js b/dom/security/test/sri/script.js new file mode 100644 index 0000000000..8fd8f96b2f --- /dev/null +++ b/dom/security/test/sri/script.js @@ -0,0 +1 @@ +var load=true; diff --git a/dom/security/test/sri/script.js^headers^ b/dom/security/test/sri/script.js^headers^ new file mode 100644 index 0000000000..b77232d81d --- /dev/null +++ b/dom/security/test/sri/script.js^headers^ @@ -0,0 +1 @@ +Cache-control: public diff --git a/dom/security/test/sri/script_301.js b/dom/security/test/sri/script_301.js new file mode 100644 index 0000000000..9a95de77cf --- /dev/null +++ b/dom/security/test/sri/script_301.js @@ -0,0 +1 @@ +var load=false; diff --git a/dom/security/test/sri/script_301.js^headers^ b/dom/security/test/sri/script_301.js^headers^ new file mode 100644 index 0000000000..efbfb73346 --- /dev/null +++ b/dom/security/test/sri/script_301.js^headers^ @@ -0,0 +1,2 @@ +HTTP 301 Moved Permanently +Location: http://example.com/tests/dom/security/test/sri/script_crossdomain5.js diff --git a/dom/security/test/sri/script_302.js b/dom/security/test/sri/script_302.js new file mode 100644 index 0000000000..9a95de77cf --- /dev/null +++ b/dom/security/test/sri/script_302.js @@ -0,0 +1 @@ +var load=false; diff --git a/dom/security/test/sri/script_302.js^headers^ b/dom/security/test/sri/script_302.js^headers^ new file mode 100644 index 0000000000..05a545a6a1 --- /dev/null +++ b/dom/security/test/sri/script_302.js^headers^ @@ -0,0 +1,2 @@ +HTTP 302 Found +Location: /tests/dom/security/test/sri/script.js diff --git a/dom/security/test/sri/script_401.js b/dom/security/test/sri/script_401.js new file mode 100644 index 0000000000..8fd8f96b2f --- /dev/null +++ b/dom/security/test/sri/script_401.js @@ -0,0 +1 @@ +var load=true; diff --git a/dom/security/test/sri/script_401.js^headers^ b/dom/security/test/sri/script_401.js^headers^ new file mode 100644 index 0000000000..889fbe081a --- /dev/null +++ b/dom/security/test/sri/script_401.js^headers^ @@ -0,0 +1,2 @@ +HTTP 401 Authorization Required +Cache-control: public diff --git a/dom/security/test/sri/script_crossdomain1.js b/dom/security/test/sri/script_crossdomain1.js new file mode 100644 index 0000000000..1f17a6db24 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain1.js @@ -0,0 +1,4 @@ +/* + * this file should be loaded, because it has CORS enabled. +*/ +window.hasCORSLoaded = true; diff --git a/dom/security/test/sri/script_crossdomain1.js^headers^ b/dom/security/test/sri/script_crossdomain1.js^headers^ new file mode 100644 index 0000000000..3a6a85d894 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain1.js^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: http://mochi.test:8888 diff --git a/dom/security/test/sri/script_crossdomain2.js b/dom/security/test/sri/script_crossdomain2.js new file mode 100644 index 0000000000..4b0208ab34 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain2.js @@ -0,0 +1,5 @@ +/* + * this file should not be loaded, because it does not have CORS + * enabled. + */ +window.hasNonCORSLoaded = true; diff --git a/dom/security/test/sri/script_crossdomain3.js b/dom/security/test/sri/script_crossdomain3.js new file mode 100644 index 0000000000..eed05d59b7 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain3.js @@ -0,0 +1 @@ +// This script intentionally left blank diff --git a/dom/security/test/sri/script_crossdomain3.js^headers^ b/dom/security/test/sri/script_crossdomain3.js^headers^ new file mode 100644 index 0000000000..3a6a85d894 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain3.js^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: http://mochi.test:8888 diff --git a/dom/security/test/sri/script_crossdomain4.js b/dom/security/test/sri/script_crossdomain4.js new file mode 100644 index 0000000000..eed05d59b7 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain4.js @@ -0,0 +1 @@ +// This script intentionally left blank diff --git a/dom/security/test/sri/script_crossdomain4.js^headers^ b/dom/security/test/sri/script_crossdomain4.js^headers^ new file mode 100644 index 0000000000..3a6a85d894 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain4.js^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: http://mochi.test:8888 diff --git a/dom/security/test/sri/script_crossdomain5.js b/dom/security/test/sri/script_crossdomain5.js new file mode 100644 index 0000000000..eed05d59b7 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain5.js @@ -0,0 +1 @@ +// This script intentionally left blank diff --git a/dom/security/test/sri/script_crossdomain5.js^headers^ b/dom/security/test/sri/script_crossdomain5.js^headers^ new file mode 100644 index 0000000000..cb762eff80 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain5.js^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: * diff --git a/dom/security/test/sri/style1.css b/dom/security/test/sri/style1.css new file mode 100644 index 0000000000..c7ab9ecffa --- /dev/null +++ b/dom/security/test/sri/style1.css @@ -0,0 +1,3 @@ +#red-text { + color: red; +} diff --git a/dom/security/test/sri/style1.css^headers^ b/dom/security/test/sri/style1.css^headers^ new file mode 100644 index 0000000000..3a6a85d894 --- /dev/null +++ b/dom/security/test/sri/style1.css^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: http://mochi.test:8888 diff --git a/dom/security/test/sri/style2.css b/dom/security/test/sri/style2.css new file mode 100644 index 0000000000..9eece75e5b --- /dev/null +++ b/dom/security/test/sri/style2.css @@ -0,0 +1 @@ +; A valid but somewhat uninteresting stylesheet diff --git a/dom/security/test/sri/style3.css b/dom/security/test/sri/style3.css new file mode 100644 index 0000000000..b64fa3b749 --- /dev/null +++ b/dom/security/test/sri/style3.css @@ -0,0 +1,3 @@ +#black-text { + color: green; +} diff --git a/dom/security/test/sri/style_301.css b/dom/security/test/sri/style_301.css new file mode 100644 index 0000000000..c7ab9ecffa --- /dev/null +++ b/dom/security/test/sri/style_301.css @@ -0,0 +1,3 @@ +#red-text { + color: red; +} diff --git a/dom/security/test/sri/style_301.css^headers^ b/dom/security/test/sri/style_301.css^headers^ new file mode 100644 index 0000000000..c5b78ee04b --- /dev/null +++ b/dom/security/test/sri/style_301.css^headers^ @@ -0,0 +1,2 @@ +HTTP 301 Moved Permanently +Location: http://example.com/tests/dom/security/test/sri/style1.css diff --git a/dom/security/test/sri/test_script_crossdomain.html b/dom/security/test/sri/test_script_crossdomain.html new file mode 100644 index 0000000000..327ea4247f --- /dev/null +++ b/dom/security/test/sri/test_script_crossdomain.html @@ -0,0 +1,18 @@ + + + + + + Cross-domain script tests for Bug 992096 + + + +Mozilla Bug 992096 +
+ +
+ + diff --git a/dom/security/test/sri/test_script_sameorigin.html b/dom/security/test/sri/test_script_sameorigin.html new file mode 100644 index 0000000000..df890d5f99 --- /dev/null +++ b/dom/security/test/sri/test_script_sameorigin.html @@ -0,0 +1,18 @@ + + + + + + Same-origin script tests for Bug 992096 + + + +Mozilla Bug 992096 +
+ +
+ + diff --git a/dom/security/test/sri/test_sri_disabled.html b/dom/security/test/sri/test_sri_disabled.html new file mode 100644 index 0000000000..4661235b10 --- /dev/null +++ b/dom/security/test/sri/test_sri_disabled.html @@ -0,0 +1,18 @@ + + + + + + security.sri.enable tests for Bug 992096 + + + +Mozilla Bug 992096 +
+ +
+ + diff --git a/dom/security/test/sri/test_style_crossdomain.html b/dom/security/test/sri/test_style_crossdomain.html new file mode 100644 index 0000000000..6bf4b2de1a --- /dev/null +++ b/dom/security/test/sri/test_style_crossdomain.html @@ -0,0 +1,18 @@ + + + + + + Cross-domain stylesheet tests for Bug 1196740 + + + +Mozilla Bug 1196740 +
+ +
+ + diff --git a/dom/security/test/sri/test_style_sameorigin.html b/dom/security/test/sri/test_style_sameorigin.html new file mode 100644 index 0000000000..fb1d3f78d2 --- /dev/null +++ b/dom/security/test/sri/test_style_sameorigin.html @@ -0,0 +1,18 @@ + + + + + + Same-origin stylesheet tests for Bug 992096 + + + +Mozilla Bug 992096 +
+ +
+ + diff --git a/dom/tests/mochitest/pointerlock/test_pointerlock-api.html b/dom/tests/mochitest/pointerlock/test_pointerlock-api.html index 9d2140f19a..c158902c64 100644 --- a/dom/tests/mochitest/pointerlock/test_pointerlock-api.html +++ b/dom/tests/mochitest/pointerlock/test_pointerlock-api.html @@ -62,6 +62,12 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=633602 "file_allowPointerLockSandboxFlag.html" ]; + var gDisableList = [ + // Bug 1174323 + { file: "file_screenClientXYConst.html", + platform: "MacIntel" } + ]; + var gTestWindow = null; var gTestIndex = 0; @@ -104,8 +110,22 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=633602 function runNextTest() { if (gTestIndex < gTestFiles.length) { - gTestWindow = window.open(gTestFiles[gTestIndex], "", "width=500,height=500"); + var file = gTestFiles[gTestIndex]; gTestIndex++; + + var skipTest = false; + for (var item of gDisableList) { + if (item.file == file && navigator.platform == item.platform) { + skipTest = true; + break; + } + } + if (!skipTest) { + info(`Testing ${file}`); + gTestWindow = window.open(file, "", "width=500,height=500"); + } else { + nextTest(); + } } else { finish(); } diff --git a/dom/webidl/HTMLLinkElement.webidl b/dom/webidl/HTMLLinkElement.webidl index 32cfb518cb..468c4db2c5 100644 --- a/dom/webidl/HTMLLinkElement.webidl +++ b/dom/webidl/HTMLLinkElement.webidl @@ -48,3 +48,8 @@ partial interface HTMLLinkElement { readonly attribute Document? import; }; +// https://w3c.github.io/webappsec/specs/subresourceintegrity/#htmllinkelement-1 +partial interface HTMLLinkElement { + [SetterThrows] + attribute DOMString integrity; +}; diff --git a/dom/webidl/HTMLScriptElement.webidl b/dom/webidl/HTMLScriptElement.webidl index cbe3161c6c..377056366d 100644 --- a/dom/webidl/HTMLScriptElement.webidl +++ b/dom/webidl/HTMLScriptElement.webidl @@ -33,3 +33,8 @@ partial interface HTMLScriptElement { attribute DOMString htmlFor; }; +// https://w3c.github.io/webappsec/specs/subresourceintegrity/#htmlscriptelement-1 +partial interface HTMLScriptElement { + [SetterThrows] + attribute DOMString integrity; +}; diff --git a/layout/style/CSSStyleSheet.cpp b/layout/style/CSSStyleSheet.cpp index 17804bd195..a62cc9269d 100644 --- a/layout/style/CSSStyleSheet.cpp +++ b/layout/style/CSSStyleSheet.cpp @@ -815,10 +815,12 @@ namespace mozilla { CSSStyleSheetInner::CSSStyleSheetInner(CSSStyleSheet* aPrimarySheet, CORSMode aCORSMode, - ReferrerPolicy aReferrerPolicy) + ReferrerPolicy aReferrerPolicy, + const SRIMetadata& aIntegrity) : mSheets() , mCORSMode(aCORSMode) , mReferrerPolicy (aReferrerPolicy) + , mIntegrity(aIntegrity) , mComplete(false) #ifdef DEBUG , mPrincipalSet(false) @@ -940,6 +942,7 @@ CSSStyleSheetInner::CSSStyleSheetInner(CSSStyleSheetInner& aCopy, mPrincipal(aCopy.mPrincipal), mCORSMode(aCopy.mCORSMode), mReferrerPolicy(aCopy.mReferrerPolicy), + mIntegrity(aCopy.mIntegrity), mComplete(aCopy.mComplete) #ifdef DEBUG , mPrincipalSet(aCopy.mPrincipalSet) @@ -1080,7 +1083,26 @@ CSSStyleSheet::CSSStyleSheet(CORSMode aCORSMode, ReferrerPolicy aReferrerPolicy) mScopeElement(nullptr), mRuleProcessors(nullptr) { - mInner = new CSSStyleSheetInner(this, aCORSMode, aReferrerPolicy); + mInner = new CSSStyleSheetInner(this, aCORSMode, aReferrerPolicy, + SRIMetadata()); +} + +CSSStyleSheet::CSSStyleSheet(CORSMode aCORSMode, + ReferrerPolicy aReferrerPolicy, + const SRIMetadata& aIntegrity) + : mTitle(), + mParent(nullptr), + mOwnerRule(nullptr), + mDocument(nullptr), + mOwningNode(nullptr), + mDisabled(false), + mDirty(false), + mInRuleProcessorCache(false), + mScopeElement(nullptr), + mRuleProcessors(nullptr) +{ + mInner = new CSSStyleSheetInner(this, aCORSMode, aReferrerPolicy, + aIntegrity); } CSSStyleSheet::CSSStyleSheet(const CSSStyleSheet& aCopy, diff --git a/layout/style/CSSStyleSheet.h b/layout/style/CSSStyleSheet.h index 45a2f4710a..d07b762d14 100644 --- a/layout/style/CSSStyleSheet.h +++ b/layout/style/CSSStyleSheet.h @@ -26,6 +26,7 @@ #include "nsCycleCollectionParticipant.h" #include "nsWrapperCache.h" #include "mozilla/net/ReferrerPolicy.h" +#include "mozilla/dom/SRIMetadata.h" class CSSRuleListImpl; class nsCSSRuleProcessor; @@ -63,7 +64,8 @@ public: private: CSSStyleSheetInner(CSSStyleSheet* aPrimarySheet, CORSMode aCORSMode, - ReferrerPolicy aReferrerPolicy); + ReferrerPolicy aReferrerPolicy, + const dom::SRIMetadata& aIntegrity); CSSStyleSheetInner(CSSStyleSheetInner& aCopy, CSSStyleSheet* aPrimarySheet); ~CSSStyleSheetInner(); @@ -96,6 +98,7 @@ private: // The Referrer Policy of a stylesheet is used for its child sheets, so it is // stored here. ReferrerPolicy mReferrerPolicy; + dom::SRIMetadata mIntegrity; bool mComplete; #ifdef DEBUG @@ -123,6 +126,8 @@ class CSSStyleSheet final : public nsIStyleSheet, public: typedef net::ReferrerPolicy ReferrerPolicy; CSSStyleSheet(CORSMode aCORSMode, ReferrerPolicy aReferrerPolicy); + CSSStyleSheet(CORSMode aCORSMode, ReferrerPolicy aReferrerPolicy, + const dom::SRIMetadata& aIntegrity); NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_AMBIGUOUS(CSSStyleSheet, @@ -259,6 +264,9 @@ public: // Get this style sheet's Referrer Policy ReferrerPolicy GetReferrerPolicy() const { return mInner->mReferrerPolicy; } + // Get this style sheet's integrity metadata + dom::SRIMetadata GetIntegrity() const { return mInner->mIntegrity; } + dom::Element* GetScopeElement() const { return mScopeElement; } void SetScopeElement(dom::Element* aScopeElement) { diff --git a/layout/style/Loader.cpp b/layout/style/Loader.cpp index 303a164dc2..c3177aae54 100644 --- a/layout/style/Loader.cpp +++ b/layout/style/Loader.cpp @@ -62,6 +62,7 @@ #include "nsError.h" #include "nsIContentSecurityPolicy.h" +#include "mozilla/dom/SRICheck.h" #include "mozilla/dom/EncodingUtils.h" using mozilla::dom::EncodingUtils; @@ -265,6 +266,16 @@ GetLoaderLog() return sLog; } +static PRLogModuleInfo* +GetSriLog() +{ + static PRLogModuleInfo *gSriPRLog; + if (!gSriPRLog) { + gSriPRLog = PR_NewLogModule("SRI"); + } + return gSriPRLog; +} + #define LOG_ERROR(args) MOZ_LOG(GetLoaderLog(), mozilla::LogLevel::Error, args) #define LOG_WARN(args) MOZ_LOG(GetLoaderLog(), mozilla::LogLevel::Warning, args) #define LOG_DEBUG(args) MOZ_LOG(GetLoaderLog(), mozilla::LogLevel::Debug, args) @@ -928,6 +939,18 @@ SheetLoadData::OnStreamComplete(nsIUnicharStreamLoader* aLoader, } } + SRIMetadata sriMetadata = mSheet->GetIntegrity(); + if (!sriMetadata.IsEmpty() && + NS_FAILED(SRICheck::VerifyIntegrity(sriMetadata, aLoader, + mSheet->GetCORSMode(), aBuffer, + mLoader->mDocument))) { + LOG((" Load was blocked by SRI")); + MOZ_LOG(GetSriLog(), mozilla::LogLevel::Debug, + ("css::Loader::OnStreamComplete, bad metadata")); + mLoader->SheetComplete(this, NS_ERROR_SRI_CORRUPT); + return NS_OK; + } + // Enough to set the URIs on mSheet, since any sibling datas we have share // the same mInner as mSheet and will thus get the same URI. mSheet->SetURIs(channelURI, originalURI, channelURI); @@ -1057,6 +1080,7 @@ Loader::CreateSheet(nsIURI* aURI, nsIPrincipal* aLoaderPrincipal, CORSMode aCORSMode, ReferrerPolicy aReferrerPolicy, + const nsAString& aIntegrity, bool aSyncLoad, bool aHasAlternateRel, const nsAString& aTitle, @@ -1204,7 +1228,17 @@ Loader::CreateSheet(nsIURI* aURI, originalURI = aURI; } - nsRefPtr sheet = new CSSStyleSheet(aCORSMode, aReferrerPolicy); + SRIMetadata sriMetadata; + if (!aIntegrity.IsEmpty()) { + MOZ_LOG(GetSriLog(), mozilla::LogLevel::Debug, + ("css::Loader::CreateSheet, integrity=%s", + NS_ConvertUTF16toUTF8(aIntegrity).get())); + SRICheck::IntegrityMetadata(aIntegrity, mDocument, &sriMetadata); + } + + nsRefPtr sheet = new CSSStyleSheet(aCORSMode, + aReferrerPolicy, + sriMetadata); sheet->SetURIs(sheetURI, originalURI, baseURI); sheet.forget(aSheet); } @@ -1415,6 +1449,8 @@ Loader::LoadSheet(SheetLoadData* aLoadData, StyleSheetState aSheetState) triggeringPrincipal = nsContentUtils::GetSystemPrincipal(); } + SRIMetadata sriMetadata = aLoadData->mSheet->GetIntegrity(); + if (aLoadData->mSyncLoad) { LOG((" Synchronous load")); NS_ASSERTION(!aLoadData->mObserver, "Observer for a sync load?"); @@ -1599,7 +1635,7 @@ Loader::LoadSheet(SheetLoadData* aLoadData, StyleSheetState aSheetState) nsCOMPtr httpChannel(do_QueryInterface(channel)); if (httpChannel) { - // send a minimal Accept header for text/css + // Send a minimal Accept header for text/css httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("Accept"), NS_LITERAL_CSTRING("text/css,*/*;q=0.1"), false); @@ -1932,8 +1968,10 @@ Loader::LoadInlineStyle(nsIContent* aElement, StyleSheetState state; nsRefPtr sheet; nsresult rv = CreateSheet(nullptr, aElement, nullptr, CORS_NONE, - mDocument->GetReferrerPolicy(), false, false, - aTitle, state, aIsAlternate, getter_AddRefs(sheet)); + mDocument->GetReferrerPolicy(), + EmptyString(), // no inline integrity checks + false, false, aTitle, state, aIsAlternate, + getter_AddRefs(sheet)); NS_ENSURE_SUCCESS(rv, rv); NS_ASSERTION(state == eSheetNeedsParser, "Inline sheets should not be cached"); @@ -1979,6 +2017,7 @@ Loader::LoadStyleLink(nsIContent* aElement, bool aHasAlternateRel, CORSMode aCORSMode, ReferrerPolicy aReferrerPolicy, + const nsAString& aIntegrity, nsICSSLoaderObserver* aObserver, bool* aIsAlternate) { @@ -2013,7 +2052,7 @@ Loader::LoadStyleLink(nsIContent* aElement, StyleSheetState state; nsRefPtr sheet; rv = CreateSheet(aURL, aElement, principal, aCORSMode, - aReferrerPolicy, false, + aReferrerPolicy, aIntegrity, false, aHasAlternateRel, aTitle, state, aIsAlternate, getter_AddRefs(sheet)); NS_ENSURE_SUCCESS(rv, rv); @@ -2177,6 +2216,7 @@ Loader::LoadChildSheet(CSSStyleSheet* aParentSheet, // For now, use CORS_NONE for child sheets rv = CreateSheet(aURL, nullptr, principal, CORS_NONE, aParentSheet->GetReferrerPolicy(), + EmptyString(), // integrity is only checked on main sheet parentData ? parentData->mSyncLoad : false, false, empty, state, &isAlternate, getter_AddRefs(sheet)); NS_ENSURE_SUCCESS(rv, rv); @@ -2243,13 +2283,14 @@ Loader::LoadSheet(nsIURI* aURL, const nsCString& aCharset, nsICSSLoaderObserver* aObserver, CORSMode aCORSMode, - ReferrerPolicy aReferrerPolicy) + ReferrerPolicy aReferrerPolicy, + const nsAString& aIntegrity) { LOG(("css::Loader::LoadSheet(aURL, aObserver) api call")); return InternalLoadNonDocumentSheet(aURL, false, false, aOriginPrincipal, aCharset, nullptr, aObserver, aCORSMode, - aReferrerPolicy); + aReferrerPolicy, aIntegrity); } nsresult @@ -2261,7 +2302,8 @@ Loader::InternalLoadNonDocumentSheet(nsIURI* aURL, CSSStyleSheet** aSheet, nsICSSLoaderObserver* aObserver, CORSMode aCORSMode, - ReferrerPolicy aReferrerPolicy) + ReferrerPolicy aReferrerPolicy, + const nsAString& aIntegrity) { NS_PRECONDITION(aURL, "Must have a URI to load"); NS_PRECONDITION(aSheet || aObserver, "Sheet and observer can't both be null"); @@ -2292,7 +2334,7 @@ Loader::InternalLoadNonDocumentSheet(nsIURI* aURL, const nsSubstring& empty = EmptyString(); rv = CreateSheet(aURL, nullptr, aOriginPrincipal, aCORSMode, - aReferrerPolicy, syncLoad, false, + aReferrerPolicy, aIntegrity, syncLoad, false, empty, state, &isAlternate, getter_AddRefs(sheet)); NS_ENSURE_SUCCESS(rv, rv); diff --git a/layout/style/Loader.h b/layout/style/Loader.h index 3446ecd92c..8602eaf1eb 100644 --- a/layout/style/Loader.h +++ b/layout/style/Loader.h @@ -223,6 +223,7 @@ public: bool aHasAlternateRel, CORSMode aCORSMode, ReferrerPolicy aReferrerPolicy, + const nsAString& aIntegrity, nsICSSLoaderObserver* aObserver, bool* aIsAlternate); @@ -320,7 +321,8 @@ public: const nsCString& aCharset, nsICSSLoaderObserver* aObserver, CORSMode aCORSMode = CORS_NONE, - ReferrerPolicy aReferrerPolicy = mozilla::net::RP_Default); + ReferrerPolicy aReferrerPolicy = mozilla::net::RP_Default, + const nsAString& aIntegrity = EmptyString()); /** * Stop loading all sheets. All nsICSSLoaderObservers involved will be @@ -417,6 +419,7 @@ private: nsIPrincipal* aLoaderPrincipal, CORSMode aCORSMode, ReferrerPolicy aReferrerPolicy, + const nsAString& aIntegrity, bool aSyncLoad, bool aHasAlternateRel, const nsAString& aTitle, @@ -450,7 +453,8 @@ private: CSSStyleSheet** aSheet, nsICSSLoaderObserver* aObserver, CORSMode aCORSMode = CORS_NONE, - ReferrerPolicy aReferrerPolicy = mozilla::net::RP_Default); + ReferrerPolicy aReferrerPolicy = mozilla::net::RP_Default, + const nsAString& aIntegrity = EmptyString()); // Post a load event for aObserver to be notified about aSheet. The // notification will be sent with status NS_OK unless the load event is diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 92c0d7ef8a..fa402566b6 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -2206,6 +2206,9 @@ pref("security.apps.privileged.CSP.default", "default-src * data: blob:; script- pref("security.mixed_content.block_active_content", false); pref("security.mixed_content.block_display_content", false); +// Sub-resource integrity +pref("security.sri.enable", false); + // Enable pinning checks by default. pref("security.cert_pinning.enforcement_level", 2); diff --git a/parser/html/nsHtml5SpeculativeLoad.cpp b/parser/html/nsHtml5SpeculativeLoad.cpp index 6930664d9d..bdc963a9fc 100644 --- a/parser/html/nsHtml5SpeculativeLoad.cpp +++ b/parser/html/nsHtml5SpeculativeLoad.cpp @@ -45,14 +45,14 @@ nsHtml5SpeculativeLoad::Perform(nsHtml5TreeOpExecutor* aExecutor) break; case eSpeculativeLoadScript: aExecutor->PreloadScript(mUrl, mCharset, mTypeOrCharsetSource, - mCrossOrigin, false); + mCrossOrigin, mIntegrity, false); break; case eSpeculativeLoadScriptFromHead: aExecutor->PreloadScript(mUrl, mCharset, mTypeOrCharsetSource, - mCrossOrigin, true); + mCrossOrigin, mIntegrity, true); break; case eSpeculativeLoadStyle: - aExecutor->PreloadStyle(mUrl, mCharset, mCrossOrigin); + aExecutor->PreloadStyle(mUrl, mCharset, mCrossOrigin, mIntegrity); break; case eSpeculativeLoadManifest: aExecutor->ProcessOfflineManifest(mUrl); diff --git a/parser/html/nsHtml5SpeculativeLoad.h b/parser/html/nsHtml5SpeculativeLoad.h index 591f039f38..0eef987440 100644 --- a/parser/html/nsHtml5SpeculativeLoad.h +++ b/parser/html/nsHtml5SpeculativeLoad.h @@ -105,6 +105,7 @@ class nsHtml5SpeculativeLoad { const nsAString& aCharset, const nsAString& aType, const nsAString& aCrossOrigin, + const nsAString& aIntegrity, bool aParserInHead) { NS_PRECONDITION(mOpCode == eSpeculativeLoadUninitialized, @@ -115,10 +116,12 @@ class nsHtml5SpeculativeLoad { mCharset.Assign(aCharset); mTypeOrCharsetSource.Assign(aType); mCrossOrigin.Assign(aCrossOrigin); + mIntegrity.Assign(aIntegrity); } inline void InitStyle(const nsAString& aUrl, const nsAString& aCharset, - const nsAString& aCrossOrigin) + const nsAString& aCrossOrigin, + const nsAString& aIntegrity) { NS_PRECONDITION(mOpCode == eSpeculativeLoadUninitialized, "Trying to reinitialize a speculative load!"); @@ -126,6 +129,7 @@ class nsHtml5SpeculativeLoad { mUrl.Assign(aUrl); mCharset.Assign(aCharset); mCrossOrigin.Assign(aCrossOrigin); + mIntegrity.Assign(aIntegrity); } /** @@ -219,6 +223,12 @@ class nsHtml5SpeculativeLoad { * attribute. If the attribute is not set, this will be a void string. */ nsString mMedia; + /** + * If mOpCode is eSpeculativeLoadScript[FromHead], this is the value of the + * "integrity" attribute. If the attribute is not set, this will be a void + * string. + */ + nsString mIntegrity; }; #endif // nsHtml5SpeculativeLoad_h diff --git a/parser/html/nsHtml5TreeBuilderCppSupplement.h b/parser/html/nsHtml5TreeBuilderCppSupplement.h index 6399c4b607..6e16a2db12 100644 --- a/parser/html/nsHtml5TreeBuilderCppSupplement.h +++ b/parser/html/nsHtml5TreeBuilderCppSupplement.h @@ -165,11 +165,14 @@ nsHtml5TreeBuilder::createElement(int32_t aNamespace, nsIAtom* aName, nsString* type = aAttributes->getValue(nsHtml5AttributeName::ATTR_TYPE); nsString* crossOrigin = aAttributes->getValue(nsHtml5AttributeName::ATTR_CROSSORIGIN); + nsString* integrity = + aAttributes->getValue(nsHtml5AttributeName::ATTR_INTEGRITY); mSpeculativeLoadQueue.AppendElement()-> InitScript(*url, (charset) ? *charset : EmptyString(), (type) ? *type : EmptyString(), (crossOrigin) ? *crossOrigin : NullString(), + (integrity) ? *integrity : NullString(), mode == NS_HTML5TREE_BUILDER_IN_HEAD); mCurrentHtmlScriptIsAsyncOrDefer = aAttributes->contains(nsHtml5AttributeName::ATTR_ASYNC) || @@ -186,10 +189,13 @@ nsHtml5TreeBuilder::createElement(int32_t aNamespace, nsIAtom* aName, nsString* charset = aAttributes->getValue(nsHtml5AttributeName::ATTR_CHARSET); nsString* crossOrigin = aAttributes->getValue(nsHtml5AttributeName::ATTR_CROSSORIGIN); + nsString* integrity = + aAttributes->getValue(nsHtml5AttributeName::ATTR_INTEGRITY); mSpeculativeLoadQueue.AppendElement()-> InitStyle(*url, (charset) ? *charset : EmptyString(), - (crossOrigin) ? *crossOrigin : NullString()); + (crossOrigin) ? *crossOrigin : NullString(), + (integrity) ? *integrity : NullString()); } } else if (rel->LowerCaseEqualsASCII("preconnect")) { nsString* url = aAttributes->getValue(nsHtml5AttributeName::ATTR_HREF); @@ -256,11 +262,14 @@ nsHtml5TreeBuilder::createElement(int32_t aNamespace, nsIAtom* aName, nsString* type = aAttributes->getValue(nsHtml5AttributeName::ATTR_TYPE); nsString* crossOrigin = aAttributes->getValue(nsHtml5AttributeName::ATTR_CROSSORIGIN); + nsString* integrity = + aAttributes->getValue(nsHtml5AttributeName::ATTR_INTEGRITY); mSpeculativeLoadQueue.AppendElement()-> InitScript(*url, EmptyString(), (type) ? *type : EmptyString(), (crossOrigin) ? *crossOrigin : NullString(), + (integrity) ? *integrity : NullString(), mode == NS_HTML5TREE_BUILDER_IN_HEAD); } } else if (nsHtml5Atoms::style == aName) { @@ -272,9 +281,12 @@ nsHtml5TreeBuilder::createElement(int32_t aNamespace, nsIAtom* aName, if (url) { nsString* crossOrigin = aAttributes->getValue(nsHtml5AttributeName::ATTR_CROSSORIGIN); + nsString* integrity = + aAttributes->getValue(nsHtml5AttributeName::ATTR_INTEGRITY); mSpeculativeLoadQueue.AppendElement()-> InitStyle(*url, EmptyString(), - (crossOrigin) ? *crossOrigin : NullString()); + (crossOrigin) ? *crossOrigin : NullString(), + (integrity) ? *integrity : NullString()); } } break; diff --git a/parser/html/nsHtml5TreeOpExecutor.cpp b/parser/html/nsHtml5TreeOpExecutor.cpp index bb6e9afe66..40b25babdf 100644 --- a/parser/html/nsHtml5TreeOpExecutor.cpp +++ b/parser/html/nsHtml5TreeOpExecutor.cpp @@ -919,6 +919,7 @@ nsHtml5TreeOpExecutor::PreloadScript(const nsAString& aURL, const nsAString& aCharset, const nsAString& aType, const nsAString& aCrossOrigin, + const nsAString& aIntegrity, bool aScriptFromHead) { nsCOMPtr uri = ConvertIfNotPreloadedYet(aURL); @@ -926,21 +927,22 @@ nsHtml5TreeOpExecutor::PreloadScript(const nsAString& aURL, return; } mDocument->ScriptLoader()->PreloadURI(uri, aCharset, aType, aCrossOrigin, - aScriptFromHead, + aIntegrity, aScriptFromHead, mSpeculationReferrerPolicy); } void nsHtml5TreeOpExecutor::PreloadStyle(const nsAString& aURL, const nsAString& aCharset, - const nsAString& aCrossOrigin) + const nsAString& aCrossOrigin, + const nsAString& aIntegrity) { nsCOMPtr uri = ConvertIfNotPreloadedYet(aURL); if (!uri) { return; } mDocument->PreloadStyle(uri, aCharset, aCrossOrigin, - mSpeculationReferrerPolicy); + mSpeculationReferrerPolicy, aIntegrity); } void diff --git a/parser/html/nsHtml5TreeOpExecutor.h b/parser/html/nsHtml5TreeOpExecutor.h index 502e1a6db8..e9f0e88681 100644 --- a/parser/html/nsHtml5TreeOpExecutor.h +++ b/parser/html/nsHtml5TreeOpExecutor.h @@ -248,10 +248,12 @@ class nsHtml5TreeOpExecutor final : public nsHtml5DocumentBuilder, const nsAString& aCharset, const nsAString& aType, const nsAString& aCrossOrigin, + const nsAString& aIntegrity, bool aScriptFromHead); void PreloadStyle(const nsAString& aURL, const nsAString& aCharset, - const nsAString& aCrossOrigin); + const nsAString& aCrossOrigin, + const nsAString& aIntegrity); void PreloadImage(const nsAString& aURL, const nsAString& aCrossOrigin, diff --git a/toolkit/components/lz4/lz4_internal.js b/toolkit/components/lz4/lz4_internal.js index 9587f634e2..6d3ac357c7 100644 --- a/toolkit/components/lz4/lz4_internal.js +++ b/toolkit/components/lz4/lz4_internal.js @@ -23,8 +23,8 @@ if (typeof Components != "undefined") { throw new Error("Please load this module with Component.utils.import or with require()"); } -let libxul = new SharedAll.Library("libxul", SharedAll.Constants.Path.libxul); -let Type = SharedAll.Type; +var libxul = new SharedAll.Library("libxul", SharedAll.Constants.Path.libxul); +var Type = SharedAll.Type; libxul.declareLazyFFI(Primitives, "compress", "workerlz4_compress", diff --git a/toolkit/content/browser-child.js b/toolkit/content/browser-child.js index cbab0d0271..e603c0586c 100644 --- a/toolkit/content/browser-child.js +++ b/toolkit/content/browser-child.js @@ -146,9 +146,20 @@ let WebProgressListener = { if (aWebProgress && aWebProgress.isTopLevel) { json.documentURI = content.document.documentURIObject.spec; + json.title = content.document.title; json.charset = content.document.characterSet; json.mayEnableCharacterEncodingMenu = docShell.mayEnableCharacterEncodingMenu; json.principal = content.document.nodePrincipal; + json.synthetic = content.document.mozSyntheticDocument; + + if (AppConstants.MOZ_CRASHREPORTER && CrashReporter.enabled) { + let uri = aLocationURI.clone(); + try { + // If the current URI contains a username/password, remove it. + uri.userPass = ""; + } catch (ex) { /* Ignore failures on about: URIs. */ } + CrashReporter.annotateCrashReport("URL", uri.spec); + } } sendAsyncMessage("Content:LocationChange", json, objects); @@ -255,6 +266,17 @@ let WebNavigation = { }, loadURI: function(uri, flags, referrer, referrerPolicy, baseURI) { + if (AppConstants.MOZ_CRASHREPORTER && CrashReporter.enabled) { + let annotation = uri; + try { + let url = Services.io.newURI(uri, null, null); + // If the current URI contains a username/password, remove it. + url.userPass = ""; + annotation = url.spec; + } catch (ex) { /* Ignore failures to parse and failures + on about: URIs. */ } + CrashReporter.annotateCrashReport("URL", annotation); + } if (referrer) referrer = Services.io.newURI(referrer, null, null); if (baseURI) @@ -336,22 +358,6 @@ addEventListener("ImageContentLoaded", function (aEvent) { } }, false); -let DocumentObserver = { - init: function() { - Services.obs.addObserver(this, "document-element-inserted", false); - addEventListener("unload", () => { - Services.obs.removeObserver(this, "document-element-inserted"); - }); - }, - - observe: function(aSubject, aTopic, aData) { - if (aSubject == content.document) { - sendAsyncMessage("DocumentInserted", {synthetic: aSubject.mozSyntheticDocument}); - } - }, -}; -DocumentObserver.init(); - const ZoomManager = { get fullZoom() { return this._cache.fullZoom; diff --git a/toolkit/content/tests/browser/browser.ini b/toolkit/content/tests/browser/browser.ini index 19f9069704..559e975baa 100644 --- a/toolkit/content/tests/browser/browser.ini +++ b/toolkit/content/tests/browser/browser.ini @@ -1,5 +1,7 @@ [DEFAULT] -support-files = head.js +support-files = + head.js + file_contentTitle.html [browser_autoscroll_disabled.js] skip-if = e10s # Bug ?????? - test touches content (getElementById on the content document) [browser_browserDrop.js] @@ -10,6 +12,7 @@ skip-if = e10s # Bug 921935 - focusmanager issues with e10s skip-if = e10s # Bug ?????? - intermittent crash of child process reported when run under e10s [browser_bug982298.js] skip-if = e10s # Bug 1064580 +[browser_contentTitle.js] [browser_default_image_filename.js] skip-if = e10s # Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly [browser_f7_caret_browsing.js] @@ -18,6 +21,9 @@ skip-if = e10s skip-if = e10s # Disabled for e10s: Bug ?????? - seems to be a timing issue with RemoteFinder.jsm messages coming later than the tests expect. [browser_input_file_tooltips.js] skip-if = e10s # Bug ?????? - test directly manipulates content (TypeError: doc.createElement is not a function) +[browser_isSynthetic.js] +support-files = + empty.png [browser_keyevents_during_autoscrolling.js] skip-if = e10s # Bug 921935 - focusmanager issues with e10s [browser_save_resend_postdata.js] @@ -26,4 +32,9 @@ support-files = data/post_form_inner.sjs data/post_form_outer.sjs skip-if = e10s # Bug ?????? - test directly manipulates content (gBrowser.contentDocument.getElementById("postForm").submit();) +[browser_content_url_annotation.js] +skip-if = !e10s || !crashreporter +support-files = + file_redirect.html + file_redirect_to.html [browser_bug1170531.js] diff --git a/toolkit/content/tests/browser/browser_contentTitle.js b/toolkit/content/tests/browser/browser_contentTitle.js new file mode 100644 index 0000000000..e99b29f205 --- /dev/null +++ b/toolkit/content/tests/browser/browser_contentTitle.js @@ -0,0 +1,16 @@ +let url = "https://example.com/browser/toolkit/content/tests/browser/file_contentTitle.html"; + +add_task(function*() { + let tab = gBrowser.selectedTab = gBrowser.addTab(url); + let browser = tab.linkedBrowser; + yield new Promise((resolve) => { + addEventListener("TestLocationChange", function listener() { + removeEventListener("TestLocationChange", listener); + resolve(); + }, true, true); + }); + + is(gBrowser.contentTitle, "Test Page", "Should have the right title."); + + gBrowser.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/browser_content_url_annotation.js b/toolkit/content/tests/browser/browser_content_url_annotation.js new file mode 100644 index 0000000000..cd42f99b0b --- /dev/null +++ b/toolkit/content/tests/browser/browser_content_url_annotation.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/*global Services, requestLongerTimeout, TestUtils, BrowserTestUtils, + ok, info, dump, is, Ci, Cu, Components, ctypes, privateNoteIntentionalCrash, + gBrowser, add_task, addEventListener, removeEventListener, ContentTask */ + +"use strict"; + +// Running this test in ASAN is slow. +requestLongerTimeout(2); + +/** + * Returns a Promise that resolves once a remote has experienced + * a crash. Resolves with the data from the .extra file (the crash annotations). + * + * @param browser + * The that will crash + * @return Promise + */ +function crashBrowser(browser) { + let kv = {}; + Cu.import("resource://gre/modules/KeyValueParser.jsm", kv); + // This frame script is injected into the remote browser, and used to + // intentionally crash the tab. We crash by using js-ctypes and dereferencing + // a bad pointer. The crash should happen immediately upon loading this + // frame script. + let frame_script = () => { + const Cu = Components.utils; + Cu.import("resource://gre/modules/ctypes.jsm"); + + let dies = function() { + privateNoteIntentionalCrash(); + let zero = new ctypes.intptr_t(8); + let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t)); + let crash = badptr.contents; + }; + + dump("Et tu, Brute?"); + dies(); + }; + + function checkSubject(subject, data) { + return subject instanceof Ci.nsIPropertyBag2 && + subject.hasKey("abnormal"); + }; + let crashPromise = TestUtils.topicObserved('ipc:content-shutdown', + checkSubject); + let crashDataPromise = crashPromise.then(([subject, data]) => { + ok(subject instanceof Ci.nsIPropertyBag2); + + let dumpID; + if ('nsICrashReporter' in Ci) { + dumpID = subject.getPropertyAsAString('dumpID'); + ok(dumpID, "dumpID is present and not an empty string"); + } + + let extra = null; + if (dumpID) { + let minidumpDirectory = getMinidumpDirectory(); + let extrafile = minidumpDirectory.clone(); + extrafile.append(dumpID + '.extra'); + ok(extrafile.exists(), 'found .extra file'); + extra = kv.parseKeyValuePairsFromFile(extrafile); + removeFile(minidumpDirectory, dumpID + '.dmp'); + removeFile(minidumpDirectory, dumpID + '.extra'); + } + + return extra; + }); + + // This frame script will crash the remote browser as soon as it is + // evaluated. + let mm = browser.messageManager; + mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", false); + return crashDataPromise; +} + +/** + * Removes a file from a directory. This is a no-op if the file does not + * exist. + * + * @param directory + * The nsIFile representing the directory to remove from. + * @param filename + * A string for the file to remove from the directory. + */ +function removeFile(directory, filename) { + let file = directory.clone(); + file.append(filename); + if (file.exists()) { + file.remove(false); + } +} + +/** + * Returns the directory where crash dumps are stored. + * + * @return nsIFile + */ +function getMinidumpDirectory() { + let dir = Services.dirsvc.get('ProfD', Ci.nsIFile); + dir.append("minidumps"); + return dir; +} + +/** + * Checks that the URL is correctly annotated on a content process crash. + */ +add_task(function* test_content_url_annotation() { + let url = "https://example.com/browser/toolkit/content/tests/browser/file_redirect.html"; + let redirect_url = "https://example.com/browser/toolkit/content/tests/browser/file_redirect_to.html"; + + yield BrowserTestUtils.withNewTab({ + gBrowser: gBrowser + }, function* (browser) { + ok(browser.isRemoteBrowser, "Should be a remote browser"); + + // file_redirect.html should send us to file_redirect_to.html + let promise = ContentTask.spawn(browser, {}, function* () { + dump('ContentTask starting...\n'); + yield new Promise((resolve) => { + addEventListener("RedirectDone", function listener() { + dump('Got RedirectDone\n'); + removeEventListener("RedirectDone", listener); + resolve(); + }, true, true); + }); + }); + browser.loadURI(url); + yield promise; + + // Crash the tab + let annotations = yield crashBrowser(browser); + + ok("URL" in annotations, "annotated a URL"); + is(annotations.URL, redirect_url, + "Should have annotated the URL after redirect"); + }); +}); diff --git a/toolkit/content/tests/browser/browser_isSynthetic.js b/toolkit/content/tests/browser/browser_isSynthetic.js new file mode 100644 index 0000000000..15a341461a --- /dev/null +++ b/toolkit/content/tests/browser/browser_isSynthetic.js @@ -0,0 +1,72 @@ +function LocationChangeListener(browser) { + this.browser = browser; + browser.addProgressListener(this); +} + +LocationChangeListener.prototype = { + wasSynthetic: false, + browser: null, + + destroy: function() { + this.browser.removeProgressListener(this); + }, + + onLocationChange: function(webProgress, request, location, flags) { + this.wasSynthetic = this.browser.isSyntheticDocument; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]) +} + +const FILES = gTestPath.replace("browser_isSynthetic.js", "") + .replace("chrome://mochitests/content/", "http://example.com/"); + +function waitForPageShow(browser) { + return ContentTask.spawn(browser, null, function*() { + Cu.import("resource://gre/modules/PromiseUtils.jsm"); + yield new Promise(resolve => { + let listener = () => { + removeEventListener("pageshow", listener, true); + resolve(); + } + addEventListener("pageshow", listener, true); + }); + }); +} + +add_task(function*() { + let tab = gBrowser.addTab("about:blank"); + let browser = tab.linkedBrowser; + yield BrowserTestUtils.browserLoaded(browser); + let listener = new LocationChangeListener(browser); + + is(browser.isSyntheticDocument, false, "Should not be synthetic"); + + let loadPromise = waitForPageShow(browser); + browser.loadURI("data:text/html;charset=utf-8,"); + yield loadPromise; + is(listener.wasSynthetic, false, "Should not be synthetic"); + is(browser.isSyntheticDocument, false, "Should not be synthetic"); + + loadPromise = waitForPageShow(browser); + browser.loadURI(FILES + "empty.png"); + yield loadPromise; + is(listener.wasSynthetic, true, "Should be synthetic"); + is(browser.isSyntheticDocument, true, "Should be synthetic"); + + loadPromise = waitForPageShow(browser); + browser.goBack(); + yield loadPromise; + is(listener.wasSynthetic, false, "Should not be synthetic"); + is(browser.isSyntheticDocument, false, "Should not be synthetic"); + + loadPromise = waitForPageShow(browser); + browser.goForward(); + yield loadPromise; + is(listener.wasSynthetic, true, "Should be synthetic"); + is(browser.isSyntheticDocument, true, "Should be synthetic"); + + listener.destroy(); + gBrowser.removeTab(tab); +}); diff --git a/toolkit/content/tests/browser/empty.png b/toolkit/content/tests/browser/empty.png new file mode 100644 index 0000000000..17ddf0c3ee Binary files /dev/null and b/toolkit/content/tests/browser/empty.png differ diff --git a/toolkit/content/tests/browser/file_contentTitle.html b/toolkit/content/tests/browser/file_contentTitle.html new file mode 100644 index 0000000000..8d330aa0f2 --- /dev/null +++ b/toolkit/content/tests/browser/file_contentTitle.html @@ -0,0 +1,14 @@ + +Test Page + + + + diff --git a/toolkit/content/tests/browser/file_redirect.html b/toolkit/content/tests/browser/file_redirect.html new file mode 100644 index 0000000000..4d5fa9dfd1 --- /dev/null +++ b/toolkit/content/tests/browser/file_redirect.html @@ -0,0 +1,13 @@ + + + + +redirecting... + + +redirectin u bro + + diff --git a/toolkit/content/tests/browser/file_redirect_to.html b/toolkit/content/tests/browser/file_redirect_to.html new file mode 100644 index 0000000000..28c0b53713 --- /dev/null +++ b/toolkit/content/tests/browser/file_redirect_to.html @@ -0,0 +1,15 @@ + + + + +redirected! + + +u got redirected, bro + + diff --git a/toolkit/content/widgets/remote-browser.xml b/toolkit/content/widgets/remote-browser.xml index 2e6a98d750..92d5b3d140 100644 --- a/toolkit/content/widgets/remote-browser.xml +++ b/toolkit/content/widgets/remote-browser.xml @@ -329,10 +329,6 @@ break; } - case "DocumentInserted": - this._isSyntheticDocument = data.synthetic; - break; - case "FullZoomChange": { this._fullZoom = data.value; let event = document.createEvent("Events"); diff --git a/toolkit/devtools/webconsole/webconsole.js b/toolkit/devtools/webconsole/webconsole.js index c1689a8d8b..b39f016793 100644 --- a/toolkit/devtools/webconsole/webconsole.js +++ b/toolkit/devtools/webconsole/webconsole.js @@ -4770,6 +4770,7 @@ var Utils = { case "CORS": case "Iframe Sandbox": case "Tracking Protection": + case "Sub-resource Integrity": return CATEGORY_SECURITY; default: diff --git a/toolkit/modules/RemoteWebProgress.jsm b/toolkit/modules/RemoteWebProgress.jsm index 33afd1b074..b8e8c2f36f 100644 --- a/toolkit/modules/RemoteWebProgress.jsm +++ b/toolkit/modules/RemoteWebProgress.jsm @@ -180,10 +180,11 @@ RemoteWebProgressManager.prototype = { this._browser.webNavigation._currentURI = location; this._browser._characterSet = json.charset; this._browser._documentURI = newURI(json.documentURI); - this._browser._contentTitle = ""; + this._browser._contentTitle = json.title; this._browser._imageDocument = null; this._browser._mayEnableCharacterEncodingMenu = json.mayEnableCharacterEncodingMenu; this._browser._contentPrincipal = json.principal; + this._browser._isSyntheticDocument = json.synthetic; this._browser._innerWindowID = json.innerWindowID; } diff --git a/toolkit/modules/addons/MatchPattern.jsm b/toolkit/modules/addons/MatchPattern.jsm new file mode 100644 index 0000000000..91d4bf6b30 --- /dev/null +++ b/toolkit/modules/addons/MatchPattern.jsm @@ -0,0 +1,120 @@ +/* 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/. */ + +"use strict"; + +const Cu = Components.utils; + +const EXPORTED_SYMBOLS = ["MatchPattern"]; + +const PERMITTED_SCHEMES = ["http", "https", "file", "ftp"]; + +// This function converts a glob pattern (containing * and possibly ? +// as wildcards) to a regular expression. +function globToRegexp(pat, allowQuestion) +{ + // Escape everything except ? and *. + pat = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&"); + + if (allowQuestion) { + pat = pat.replace(/\?/g, "."); + } else { + pat = pat.replace(/\?/g, "\\?"); + } + pat = pat.replace(/\*/g, ".*"); + return new RegExp("^" + pat + "$"); +} + +// These patterns follow the syntax in +// https://developer.chrome.com/extensions/match_patterns +function SingleMatchPattern(pat) +{ + if (pat == "") { + this.scheme = PERMITTED_SCHEMES; + this.host = "*"; + this.path = new RegExp('.*'); + } else if (!pat) { + this.scheme = []; + } else { + let re = new RegExp("^(http|https|file|ftp|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+|)(/.*)$"); + let match = re.exec(pat); + if (!match) { + Cu.reportError(`Invalid match pattern: '${pat}'`); + this.scheme = []; + return; + } + + if (match[1] == '*') { + this.scheme = ["http", "https"]; + } else { + this.scheme = [match[1]]; + } + this.host = match[2]; + this.path = globToRegexp(match[3], false); + + // We allow the host to be empty for file URLs. + if (this.host == "" && this.scheme[0] != "file") { + Cu.reportError(`Invalid match pattern: '${pat}'`); + this.scheme = []; + return; + } + } +} + +SingleMatchPattern.prototype = { + matches(uri) { + if (this.scheme.indexOf(uri.scheme) == -1) { + return false; + } + + // This code ignores the port, as Chrome does. + if (this.host == '*') { + // Don't check anything. + } else if (this.host[0] == '*') { + // It must be *.foo. We also need to allow foo by itself. + let suffix = this.host.substr(2); + if (uri.host != suffix && !uri.host.endsWith("." + suffix)) { + return false; + } + } else { + if (this.host != uri.host) { + return false; + } + } + + if (!this.path.test(uri.path)) { + return false; + } + + return true; + } +}; + +function MatchPattern(pat) +{ + this.pat = pat; + if (!pat) { + this.matchers = []; + } else if (pat instanceof String || typeof(pat) == "string") { + this.matchers = [new SingleMatchPattern(pat)]; + } else { + this.matchers = [for (p of pat) new SingleMatchPattern(p)]; + } +} + +MatchPattern.prototype = { + // |uri| should be an nsIURI. + matches(uri) { + for (let matcher of this.matchers) { + if (matcher.matches(uri)) { + return true; + } + } + return false; + }, + + serialize() { + return this.pat; + }, +}; diff --git a/toolkit/modules/addons/WebRequest.jsm b/toolkit/modules/addons/WebRequest.jsm new file mode 100644 index 0000000000..13230a2b95 --- /dev/null +++ b/toolkit/modules/addons/WebRequest.jsm @@ -0,0 +1,450 @@ +/* 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/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["WebRequest"]; + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "WebRequestCommon", + "resource://gre/modules/WebRequestCommon.jsm"); + +// TODO +// Figure out how to handle requestId. Gecko seems to have no such thing. (Bug 1163862) +// We also don't know the method for content policy. (Bug 1163862) +// We don't even have a window ID for HTTP observer stuff. (Bug 1163861) + +function parseFilter(filter) { + if (!filter) { + filter = {}; + } + + // FIXME: Support windowId filtering. + return {urls: filter.urls || null, types: filter.types || null}; +} + +function parseExtra(extra, allowed) { + if (extra) { + for (let ex of extra) { + if (allowed.indexOf(ex) == -1) { + throw `Invalid option ${ex}`; + } + } + } + + let result = {}; + for (let al of allowed) { + if (extra && extra.indexOf(al) != -1) { + result[al] = true; + } + } + return result; +} + +let ContentPolicyManager = { + policyData: new Map(), + policies: new Map(), + idMap: new Map(), + nextId: 0, + + init() { + Services.ppmm.initialProcessData.webRequestContentPolicies = this.policyData; + + Services.ppmm.addMessageListener("WebRequest:ShouldLoad", this); + Services.mm.addMessageListener("WebRequest:ShouldLoad", this); + }, + + receiveMessage(msg) { + let browser = msg.target instanceof Ci.nsIDOMXULElement ? msg.target : null; + + for (let id of msg.data.ids) { + let callback = this.policies.get(id); + if (!callback) { + // It's possible that this listener has been removed and the + // child hasn't learned yet. + continue; + } + let response = null; + try { + response = callback({ + url: msg.data.url, + windowId: msg.data.windowId, + parentWindowId: msg.data.parentWindowId, + type: msg.data.type, + browser: browser + }); + } catch (e) { + Cu.reportError(e); + } + + if (response && response.cancel) { + return {cancel: true}; + } + + // FIXME: Need to handle redirection here. (Bug 1163862) + } + + return {}; + }, + + addListener(callback, opts) { + let id = this.nextId++; + opts.id = id; + if (opts.filter.urls) { + opts.filter.urls = opts.filter.urls.serialize(); + } + Services.ppmm.broadcastAsyncMessage("WebRequest:AddContentPolicy", opts); + + this.policyData.set(id, opts); + + this.policies.set(id, callback); + this.idMap.set(callback, id); + }, + + removeListener(callback) { + let id = this.idMap.get(callback); + Services.ppmm.broadcastAsyncMessage("WebRequest:RemoveContentPolicy", {id}); + + this.policyData.delete(id); + this.idMap.delete(callback); + this.policies.delete(id); + }, +}; +ContentPolicyManager.init(); + +function StartStopListener(manager) +{ + this.manager = manager; + this.orig = null; +} + +StartStopListener.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver, + Ci.nsIStreamListener, + Ci.nsISupports]), + + onStartRequest: function(request, context) { + this.manager.onStartRequest(request); + return this.orig.onStartRequest(request, context); + }, + + onStopRequest(request, context, statusCode) { + let result = this.orig.onStopRequest(request, context, statusCode); + this.manager.onStopRequest(request); + return result; + }, + + onDataAvailable(...args) { + return this.orig.onDataAvailable(...args); + } +}; + +let HttpObserverManager = { + modifyInitialized: false, + examineInitialized: false, + + listeners: { + modify: new Map(), + afterModify: new Map(), + headersReceived: new Map(), + onStart: new Map(), + onStop: new Map(), + }, + + addOrRemove() { + let needModify = this.listeners.modify.size || this.listeners.afterModify.size; + if (needModify && !this.modifyInitialized) { + this.modifyInitialized = true; + Services.obs.addObserver(this, "http-on-modify-request", false); + } else if (!needModify && this.modifyInitialized) { + this.modifyInitialized = false; + Services.obs.removeObserver(this, "http-on-modify-request"); + } + + let needExamine = this.listeners.headersReceived.size || + this.listeners.onStart.size || + this.listeners.onStop.size; + if (needExamine && !this.examineInitialized) { + this.examineInitialized = true; + Services.obs.addObserver(this, "http-on-examine-response", false); + Services.obs.addObserver(this, "http-on-examine-cached-response", false); + Services.obs.addObserver(this, "http-on-examine-merged-response", false); + } else if (!needExamine && this.examineInitialized) { + this.examineInitialized = false; + Services.obs.removeObserver(this, "http-on-examine-response"); + Services.obs.removeObserver(this, "http-on-examine-cached-response"); + Services.obs.removeObserver(this, "http-on-examine-merged-response"); + } + }, + + addListener(kind, callback, opts) { + this.listeners[kind].set(callback, opts); + this.addOrRemove(); + }, + + removeListener(kind, callback) { + this.listeners[kind].delete(callback); + this.addOrRemove(); + }, + + getLoadContext(channel) { + try { + return channel.QueryInterface(Ci.nsIChannel) + .notificationCallbacks + .getInterface(Components.interfaces.nsILoadContext); + } catch (e) { + try { + return channel.loadGroup + .notificationCallbacks + .getInterface(Components.interfaces.nsILoadContext); + } catch (e) { + return null; + } + } + }, + + getHeaders(channel, method) { + let headers = []; + let visitor = { + visitHeader(name, value) { + headers.push({name, value}); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIHttpHeaderVisitor, + Ci.nsISupports]), + }; + + channel[method](visitor); + return headers; + }, + + observe(subject, topic, data) { + if (topic == "http-on-modify-request") { + this.modify(subject, topic, data); + } else if (topic == "http-on-examine-response" || + topic == "http-on-examine-cached-response" || + topic == "http-on-examine-merged-response") { + this.examine(subject, topic, data); + } + }, + + shouldRunListener(policyType, uri, filter) { + return WebRequestCommon.typeMatches(policyType, filter.types) && + WebRequestCommon.urlMatches(uri, filter.urls); + }, + + runChannelListener(request, kind) { + let listeners = this.listeners[kind]; + let channel = request.QueryInterface(Ci.nsIHttpChannel); + let loadContext = this.getLoadContext(channel); + let browser = loadContext ? loadContext.topFrameElement : null; + let loadInfo = channel.loadInfo; + let policyType = loadInfo.contentPolicyType; + + let requestHeaders; + let responseHeaders; + + let includeStatus = kind == "headersReceived" || kind == "onStart" || kind == "onStop"; + + for (let [callback, opts] of listeners.entries()) { + if (!this.shouldRunListener(policyType, channel.URI, opts.filter)) { + continue; + } + + let data = { + url: channel.URI.spec, + method: channel.requestMethod, + browser: browser, + type: WebRequestCommon.typeForPolicyType(policyType), + windowId: loadInfo.outerWindowID, + parentWindowId: loadInfo.parentOuterWindowID, + }; + if (opts.requestHeaders) { + if (!requestHeaders) { + requestHeaders = this.getHeaders(channel, "visitRequestHeaders"); + } + data.requestHeaders = requestHeaders; + } + if (opts.responseHeaders) { + if (!responseHeaders) { + responseHeaders = this.getHeaders(channel, "visitResponseHeaders"); + } + data.responseHeaders = responseHeaders; + } + if (includeStatus) { + data.statusCode = channel.responseStatus; + } + + let result = null; + try { + result = callback(data); + } catch (e) { + Cu.reportError(e); + } + + if (!result || !opts.blocking) { + return true; + } + if (result.cancel) { + channel.cancel(); + return false; + } + if (result.redirectUrl) { + channel.redirectTo(BrowserUtils.makeURI(result.redirectUrl)); + return false; + } + if (opts.requestHeaders && result.requestHeaders) { + // Start by clearing everything. + for (let {name, value} of requestHeaders) { + channel.setRequestHeader(name, "", false); + } + + for (let {name, value} of result.requestHeaders) { + channel.setRequestHeader(name, value, false); + } + } + if (opts.responseHeaders && result.responseHeaders) { + // Start by clearing everything. + for (let {name, value} of responseHeaders) { + channel.setResponseHeader(name, "", false); + } + + for (let {name, value} of result.responseHeaders) { + channel.setResponseHeader(name, value, false); + } + } + } + }, + + modify(subject, topic, data) { + if (this.runChannelListener(subject, "modify")) { + this.runChannelListener(subject, "afterModify"); + } + }, + + examine(subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (this.listeners.onStart.size || this.listeners.onStop.size) { + if (channel instanceof Components.interfaces.nsITraceableChannel) { + let listener = new StartStopListener(this); + let orig = subject.setNewListener(listener); + listener.orig = orig; + } + } + + this.runChannelListener(subject, "headersReceived"); + }, + + onStartRequest(request) { + this.runChannelListener(request, "onStart"); + }, + + onStopRequest(request) { + this.runChannelListener(request, "onStop"); + }, +}; + +let onBeforeRequest = { + addListener(callback, filter = null, opt_extraInfoSpec = null) { + // FIXME: Add requestBody support. + let opts = parseExtra(opt_extraInfoSpec, ["blocking"]); + opts.filter = parseFilter(filter); + ContentPolicyManager.addListener(callback, opts); + }, + + removeListener(callback) { + ContentPolicyManager.removeListener(callback); + } +}; + +let onBeforeSendHeaders = { + addListener(callback, filter = null, opt_extraInfoSpec = null) { + let opts = parseExtra(opt_extraInfoSpec, ["requestHeaders", "blocking"]); + opts.filter = parseFilter(filter); + HttpObserverManager.addListener("modify", callback, opts); + }, + + removeListener(callback) { + HttpObserverManager.removeListener("modify", callback); + } +}; + +let onSendHeaders = { + addListener(callback, filter = null, opt_extraInfoSpec = null) { + let opts = parseExtra(opt_extraInfoSpec, ["requestHeaders"]); + opts.filter = parseFilter(filter); + HttpObserverManager.addListener("afterModify", callback, opts); + }, + + removeListener(callback) { + HttpObserverManager.removeListener("afterModify", callback); + } +}; + +let onHeadersReceived = { + addListener(callback, filter = null, opt_extraInfoSpec = null) { + let opts = parseExtra(opt_extraInfoSpec, ["blocking", "responseHeaders"]); + opts.filter = parseFilter(filter); + HttpObserverManager.addListener("headersReceived", callback, opts); + }, + + removeListener(callback) { + HttpObserverManager.removeListener("headersReceived", callback); + } +}; + +let onResponseStarted = { + addListener(callback, filter = null, opt_extraInfoSpec = null) { + let opts = parseExtra(opt_extraInfoSpec, ["responseHeaders"]); + opts.filter = parseFilter(filter); + HttpObserverManager.addListener("onStart", callback, opts); + }, + + removeListener(callback) { + HttpObserverManager.removeListener("onStart", callback); + } +}; + +let onCompleted = { + addListener(callback, filter = null, opt_extraInfoSpec = null) { + let opts = parseExtra(opt_extraInfoSpec, ["responseHeaders"]); + opts.filter = parseFilter(filter); + HttpObserverManager.addListener("onStop", callback, opts); + }, + + removeListener(callback) { + HttpObserverManager.removeListener("onStop", callback); + } +}; + +let WebRequest = { + // Handled via content policy. + onBeforeRequest: onBeforeRequest, + + // http-on-modify observer. + onBeforeSendHeaders: onBeforeSendHeaders, + + // http-on-modify observer. + onSendHeaders: onSendHeaders, + + // http-on-examine-*observer. + onHeadersReceived: onHeadersReceived, + + // OnStartRequest channel listener. + onResponseStarted: onResponseStarted, + + // OnStopRequest channel listener. + onCompleted: onCompleted, +}; + +Services.ppmm.loadProcessScript("resource://gre/modules/WebRequestContent.js", true); diff --git a/toolkit/modules/addons/WebRequestCommon.jsm b/toolkit/modules/addons/WebRequestCommon.jsm new file mode 100644 index 0000000000..a7dd9c6820 --- /dev/null +++ b/toolkit/modules/addons/WebRequestCommon.jsm @@ -0,0 +1,42 @@ +/* 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/. */ + +const EXPORTED_SYMBOLS = ["WebRequestCommon"]; + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; +const Cr = Components.results; + +let WebRequestCommon = { + typeForPolicyType(type) { + switch (type) { + case Ci.nsIContentPolicy.TYPE_DOCUMENT: return "main_frame"; + case Ci.nsIContentPolicy.TYPE_SUBDOCUMENT: return "sub_frame"; + case Ci.nsIContentPolicy.TYPE_STYLESHEET: return "stylesheet"; + case Ci.nsIContentPolicy.TYPE_SCRIPT: return "script"; + case Ci.nsIContentPolicy.TYPE_IMAGE: return "image"; + case Ci.nsIContentPolicy.TYPE_OBJECT: return "object"; + case Ci.nsIContentPolicy.TYPE_OBJECT_SUBREQUEST: return "object"; + case Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST: return "xmlhttprequest"; + default: return "other"; + } + }, + + typeMatches(policyType, filterTypes) { + if (filterTypes === null) { + return true; + } + + return filterTypes.indexOf(this.typeForPolicyType(policyType)) != -1; + }, + + urlMatches(uri, urlFilter) { + if (urlFilter === null) { + return true; + } + + return urlFilter.matches(uri); + } +}; diff --git a/toolkit/modules/addons/WebRequestContent.js b/toolkit/modules/addons/WebRequestContent.js new file mode 100644 index 0000000000..2cd019a3bd --- /dev/null +++ b/toolkit/modules/addons/WebRequestContent.js @@ -0,0 +1,176 @@ +/* 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/. */ + +"use strict"; + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern", + "resource://gre/modules/MatchPattern.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "WebRequestCommon", + "resource://gre/modules/WebRequestCommon.jsm"); + +let ContentPolicy = { + _classDescription: "WebRequest content policy", + _classID: Components.ID("938e5d24-9ccc-4b55-883e-c252a41f7ce9"), + _contractID: "@mozilla.org/webrequest/policy;1", + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy, + Ci.nsIFactory, + Ci.nsISupportsWeakReference]), + + init() { + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory(this._classID, this._classDescription, this._contractID, this); + + this.contentPolicies = new Map(); + Services.cpmm.addMessageListener("WebRequest:AddContentPolicy", this); + Services.cpmm.addMessageListener("WebRequest:RemoveContentPolicy", this); + + if (initialProcessData && initialProcessData.webRequestContentPolicies) { + for (let data of initialProcessData.webRequestContentPolicies.values()) { + this.addContentPolicy(data); + } + } + }, + + addContentPolicy({id, blocking, filter}) { + if (this.contentPolicies.size == 0) { + this.register(); + } + if (filter.urls) { + filter.urls = new MatchPattern(filter.urls); + } + this.contentPolicies.set(id, {blocking, filter}); + }, + + receiveMessage(msg) { + switch (msg.name) { + case "WebRequest:AddContentPolicy": + this.addContentPolicy(msg.data); + break; + + case "WebRequest:RemoveContentPolicy": + this.contentPolicies.delete(msg.data.id); + if (this.contentPolicies.size == 0) { + this.unregister(); + } + break; + } + }, + + register() { + let catMan = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager); + catMan.addCategoryEntry("content-policy", this._contractID, this._contractID, false, true); + }, + + unregister() { + let catMan = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager); + catMan.deleteCategoryEntry("content-policy", this._contractID, false); + }, + + shouldLoad(policyType, contentLocation, requestOrigin, + node, mimeTypeGuess, extra, requestPrincipal) { + let block = false; + let ids = []; + for (let [id, {blocking, filter}] of this.contentPolicies.entries()) { + if (WebRequestCommon.typeMatches(policyType, filter.types) && + WebRequestCommon.urlMatches(contentLocation, filter.urls)) + { + if (blocking) { + block = true; + } + ids.push(id); + } + } + + if (!ids.length) { + return Ci.nsIContentPolicy.ACCEPT; + } + + let windowId = 0; + let parentWindowId = -1; + let mm = Services.cpmm; + + function getWindowId(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + } + + if (policyType == Ci.nsIContentPolicy.TYPE_SUBDOCUMENT || + (node instanceof Ci.nsIDOMXULElement && node.localName == "browser")) + { + // Chrome sets frameId to the ID of the sub-window. But when + // Firefox loads an iframe, it sets |node| to the