diff --git a/AUTHORS b/AUTHORS index 2d2c6155500..7e9d1f49124 100644 --- a/AUTHORS +++ b/AUTHORS @@ -359,6 +359,7 @@ Florian Scholz France Telecom Research and Development Franck +Francois Marier Frank Tang Frank Yan Franky Braem diff --git a/browser/devtools/webconsole/webconsole.js b/browser/devtools/webconsole/webconsole.js index 57b0cb8dfbd..30334403a7c 100644 --- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -4807,6 +4807,7 @@ var Utils = { case "CORS": case "Iframe Sandbox": case "Tracking Protection": + case "Sub-resource Integrity": return CATEGORY_SECURITY; default: diff --git a/dom/base/moz.build b/dom/base/moz.build index f383d804bd3..d541ce17f57 100644 --- a/dom/base/moz.build +++ b/dom/base/moz.build @@ -432,6 +432,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 eab48e46a2c..85446190ffb 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; @@ -750,12 +760,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 df8d730a6bf..d971764ef3b 100644 --- a/dom/base/nsDocument.cpp +++ b/dom/base/nsDocument.cpp @@ -9876,7 +9876,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(); @@ -9886,7 +9887,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 70407924635..96062f7c4a4 100644 --- a/dom/base/nsDocument.h +++ b/dom/base/nsDocument.h @@ -1147,7 +1147,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 d1fcbaeb556..5b6229740c2 100644 --- a/dom/base/nsGkAtomList.h +++ b/dom/base/nsGkAtomList.h @@ -489,6 +489,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 d785173809a..4738120b0f2 100644 --- a/dom/base/nsIDocument.h +++ b/dom/base/nsIDocument.h @@ -152,8 +152,8 @@ typedef CallbackObjectHolder NodeFilterHolder; } // namespace mozilla #define NS_IDOCUMENT_IID \ -{ 0xbbce44c8, 0x22fe, 0x404f, \ - { 0x9e, 0x71, 0x23, 0x1d, 0xf4, 0xcc, 0x8e, 0x34 } } +{ 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 { @@ -2023,7 +2023,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 55bbfd1ccc0..22557e81392 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; @@ -606,7 +618,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; @@ -720,7 +747,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; @@ -1408,8 +1436,15 @@ 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, + request->mURI, + 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. @@ -1603,6 +1638,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) { @@ -1611,9 +1647,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 b951d8e0c6f..2cc68cef7fc 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), @@ -71,6 +73,7 @@ public: mJSVersion(aVersion), mLineNo(1), mCORSMode(aCORSMode), + mIntegrity(aIntegrity), mReferrerPolicy(mozilla::net::RP_Default) { } @@ -122,6 +125,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; }; @@ -367,11 +371,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 e01151b1a8d..11164c5d86f 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/html/HTMLLinkElement.cpp b/dom/html/HTMLLinkElement.cpp index 66ff6dba71c..87125c3ce0e 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 f4f099a5d23..943b68fdba9 100644 --- a/dom/html/HTMLLinkElement.h +++ b/dom/html/HTMLLinkElement.h @@ -143,6 +143,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 aa0837252bd..7c1cd7d7029 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 5de8785a996..f12866bd0a6 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 f139369df7e..40be53b1416 100644 --- a/dom/locales/en-US/chrome/security/security.properties +++ b/dom/locales/en-US/chrome/security/security.properties @@ -53,6 +53,21 @@ 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" +MalformedIntegrityURI=The script element has a malformed URI 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" +IntegrityMismatch=None of the "%1$S" hashes in the integrity attribute match the content of the subresource. +IneligibleResource="%1$S" is not eligible for integrity checks since it's neither CORS-enabled nor same-origin. +# LOCALIZATION NOTE: Do not translate "integrity" +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 00000000000..f495a261027 --- /dev/null +++ b/dom/security/SRICheck.cpp @@ -0,0 +1,295 @@ +/* -*- 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 "nsICryptoHash.h" +#include "nsIDocument.h" +#include "nsIProtocolHandler.h" +#include "nsIScriptError.h" +#include "nsIScriptSecurityManager.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(nsIURI* aRequestURI, const CORSMode aCORSMode, + const nsIDocument* aDocument) +{ + NS_ENSURE_ARG_POINTER(aRequestURI); + NS_ENSURE_ARG_POINTER(aDocument); + + nsAutoCString requestSpec; + nsresult rv = aRequestURI->GetSpec(requestSpec); + NS_ENSURE_SUCCESS(rv, rv); + NS_ConvertUTF8toUTF16 requestSpecUTF16(requestSpec); + + // Was the sub-resource loaded via CORS? + if (aCORSMode != CORS_NONE) { + SRILOG(("SRICheck::IsEligible, CORS mode")); + return NS_OK; + } + + // Is the sub-resource same-origin? + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + if (NS_SUCCEEDED(ssm->CheckSameOriginURI(aDocument->GetDocumentURI(), + aRequestURI, false))) { + SRILOG(("SRICheck::IsEligible, same-origin")); + return NS_OK; + } + if (MOZ_LOG_TEST(GetSriLog(), mozilla::LogLevel::Debug)) { + nsAutoCString documentURI; + aDocument->GetDocumentURI()->GetAsciiSpec(documentURI); + // documentURI will be empty if GetAsciiSpec failed + SRILOG(("SRICheck::IsEligible, NOT same origin: documentURI=%s; requestURI=%s", + documentURI.get(), requestSpec.get())); + } + + 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, + "MalformedIntegrityURI", + 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 +SRICheck::VerifyIntegrity(const SRIMetadata& aMetadata, + nsIURI* aRequestURI, + const CORSMode aCORSMode, + const nsAString& aString, + const nsIDocument* aDocument) +{ + NS_ConvertUTF16toUTF8 utf8Hash(aString); + return VerifyIntegrity(aMetadata, aRequestURI, aCORSMode, utf8Hash.Length(), + (uint8_t*)utf8Hash.get(), aDocument); +} + +/* static */ nsresult +SRICheck::VerifyIntegrity(const SRIMetadata& aMetadata, + nsIURI* aRequestURI, + const CORSMode aCORSMode, + uint32_t aStringLen, + const uint8_t* aString, + const nsIDocument* aDocument) +{ + if (MOZ_LOG_TEST(GetSriLog(), mozilla::LogLevel::Debug)) { + nsAutoCString requestURL; + aRequestURI->GetAsciiSpec(requestURL); + // requestURL will be empty if GetAsciiSpec fails + SRILOG(("SRICheck::VerifyIntegrity, url=%s (length=%u)", + requestURL.get(), aStringLen)); + } + + 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(aRequestURI, aCORSMode, aDocument))) { + return NS_OK; // ignore non-CORS resources for forward-compatibility + } + 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; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/security/SRICheck.h b/dom/security/SRICheck.h new file mode 100644 index 00000000000..692f58c4519 --- /dev/null +++ b/dom/security/SRICheck.h @@ -0,0 +1,62 @@ +/* -*- 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 nsIHttpChannel; +class nsIScriptSecurityManager; +class nsIStreamLoader; +class nsIURI; + +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, + nsIURI* aRequestURI, + 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, + nsIURI* aRequestURI, + 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 00000000000..72b05769184 --- /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 00000000000..4a37c646a3a --- /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 1577c17512b..e11c26eeaac 100644 --- a/dom/security/moz.build +++ b/dom/security/moz.build @@ -12,6 +12,8 @@ EXPORTS.mozilla.dom += [ 'nsCSPService.h', 'nsCSPUtils.h', 'nsMixedContentBlocker.h', + 'SRICheck.h', + 'SRIMetadata.h', ] EXPORTS += [ @@ -27,6 +29,8 @@ UNIFIED_SOURCES += [ 'nsCSPService.cpp', 'nsCSPUtils.cpp', 'nsMixedContentBlocker.cpp', + 'SRICheck.cpp', + 'SRIMetadata.cpp', ] FAIL_ON_WARNINGS = True diff --git a/dom/webidl/HTMLLinkElement.webidl b/dom/webidl/HTMLLinkElement.webidl index 32cfb518cb4..468c4db2c54 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 cbe3161c6c4..377056366da 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 718de62b5d0..f7dc4dcc117 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 45a2f4710ab..d07b762d147 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 8b7d7de4f8f..21cd5b30694 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, channelURI, + 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 5cd4f8d6d8d..cd17ebeb66e 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 9e953d83546..92571a1a750 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -1958,6 +1958,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); + // Disable pinning checks by default. pref("security.cert_pinning.enforcement_level", 0); // Do not process hpkp headers rooted by not built in roots by default. diff --git a/parser/html/nsHtml5SpeculativeLoad.cpp b/parser/html/nsHtml5SpeculativeLoad.cpp index 6930664d9d5..bdc963a9fc0 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 591f039f384..0eef9874400 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 6399c4b607d..6e16a2db122 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 e6197d67e72..1b66f52dc89 100644 --- a/parser/html/nsHtml5TreeOpExecutor.cpp +++ b/parser/html/nsHtml5TreeOpExecutor.cpp @@ -911,6 +911,7 @@ nsHtml5TreeOpExecutor::PreloadScript(const nsAString& aURL, const nsAString& aCharset, const nsAString& aType, const nsAString& aCrossOrigin, + const nsAString& aIntegrity, bool aScriptFromHead) { nsCOMPtr uri = ConvertIfNotPreloadedYet(aURL); @@ -918,21 +919,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 502e1a6db84..e9f0e886815 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/xpcom/base/ErrorList.h b/xpcom/base/ErrorList.h index e05c9dd70b2..0ed10aece55 100644 --- a/xpcom/base/ErrorList.h +++ b/xpcom/base/ErrorList.h @@ -657,6 +657,11 @@ ERROR(NS_ERROR_CSP_FORM_ACTION_VIOLATION, FAILURE(98)), ERROR(NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION, FAILURE(99)), + /* Error code for Sub-Resource Integrity */ + ERROR(NS_ERROR_SRI_CORRUPT, FAILURE(200)), + ERROR(NS_ERROR_SRI_DISABLED, FAILURE(201)), + ERROR(NS_ERROR_SRI_NOT_ELIGIBLE, FAILURE(202)), + /* CMS specific nsresult error codes. Note: the numbers used here correspond * to the values in nsICMSMessageErrors.idl. */ ERROR(NS_ERROR_CMS_VERIFY_NOT_SIGNED, FAILURE(1024)),