From 6917b697b8b2b08f875b3a5311653f8d679e94af Mon Sep 17 00:00:00 2001 From: Sebastian Streich Date: Wed, 31 Jul 2019 16:59:53 +0000 Subject: [PATCH] Bug 1428473 Support X-Content-Type-Options: nosniff when navigating r=ckerschb,dragana,alchen *** Apply Requested Revision Differential Revision: https://phabricator.services.mozilla.com/D33959 --HG-- extra : moz-landing-system : lando --- .../test/general/file_nosniff_navigation.sjs | 32 +++++++++ .../file_nosniff_navigation_garbage.sjs | 33 ++++++++++ .../file_nosniff_navigation_mismatch.sjs | 33 ++++++++++ dom/security/test/general/mochitest.ini | 4 ++ .../test/general/test_nosniff_navigation.html | 65 +++++++++++++++++++ ipc/glue/BackgroundUtils.cpp | 8 ++- netwerk/base/LoadInfo.cpp | 18 ++++- netwerk/base/LoadInfo.h | 4 +- netwerk/base/nsILoadInfo.idl | 9 +++ netwerk/base/nsNetUtil.cpp | 9 +++ netwerk/ipc/NeckoChannelParams.ipdlh | 7 ++ netwerk/protocol/http/nsHttpChannel.cpp | 11 ++++ .../converters/nsUnknownDecoder.cpp | 34 +++++++++- parser/html/nsHtml5StreamParser.cpp | 13 +++- parser/html/nsHtml5StreamParser.h | 5 ++ 15 files changed, 279 insertions(+), 6 deletions(-) create mode 100644 dom/security/test/general/file_nosniff_navigation.sjs create mode 100644 dom/security/test/general/file_nosniff_navigation_garbage.sjs create mode 100644 dom/security/test/general/file_nosniff_navigation_mismatch.sjs create mode 100644 dom/security/test/general/test_nosniff_navigation.html diff --git a/dom/security/test/general/file_nosniff_navigation.sjs b/dom/security/test/general/file_nosniff_navigation.sjs new file mode 100644 index 000000000000..8d1de13828b6 --- /dev/null +++ b/dom/security/test/general/file_nosniff_navigation.sjs @@ -0,0 +1,32 @@ +// Custom *.sjs file specifically for the needs of Bug 1286861 + +// small red image +const IMG = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="); + +function getSniffableContent(selector){ + switch(selector){ + case "xml": + return ``; + case "html": + return ` Test test `; + case "css": + return `*{ color: pink !important; }`; + case 'json': + return `{ 'test':'yes' }`; + case 'img': + return IMG; + } + return "Basic UTF-8 Text"; +} + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader('X-Content-Type-Options', 'nosniff'); // Disable Sniffing + response.setHeader("Content-Type","*/*"); // Try Browser to force sniffing. + response.write(getSniffableContent(request.queryString)); + return; +} + diff --git a/dom/security/test/general/file_nosniff_navigation_garbage.sjs b/dom/security/test/general/file_nosniff_navigation_garbage.sjs new file mode 100644 index 000000000000..726c6ecf9ebc --- /dev/null +++ b/dom/security/test/general/file_nosniff_navigation_garbage.sjs @@ -0,0 +1,33 @@ +// Custom *.sjs file specifically for the needs of Bug 1286861 + +// small red image +const IMG = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="); + +function getSniffableContent(selector){ + switch(selector){ + case "xml": + return ``; + case "html": + return ` Test test `; + case 'js': + return `` + case "css": + return `*{ color: pink !important; }`; + case 'json': + return `{ 'test':'yes' }`; + case 'img': + return IMG; + } + return "Basic UTF-8 Text"; +} + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader('X-Content-Type-Options', 'nosniff'); // Disable Sniffing + response.setHeader("Content-Type","garbage/garbage"); // Try Browser to force sniffing. + response.write(getSniffableContent(request.queryString)); + return; +} diff --git a/dom/security/test/general/file_nosniff_navigation_mismatch.sjs b/dom/security/test/general/file_nosniff_navigation_mismatch.sjs new file mode 100644 index 000000000000..40043a78dc6f --- /dev/null +++ b/dom/security/test/general/file_nosniff_navigation_mismatch.sjs @@ -0,0 +1,33 @@ +// Custom *.sjs file specifically for the needs of Bug 1286861 + +// small red image +const IMG = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="); + +function getSniffableContent(selector){ + switch(selector){ + case "xml": + return ``; + case "html": + return ` Test test `; + case 'js': + return `` + case "css": + return `*{ color: pink !important; }`; + case 'json': + return `{ 'test':'yes' }`; + case 'img': + return IMG; + } + return "Basic UTF-8 Text"; +} + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader('X-Content-Type-Options', 'nosniff'); // Disable Sniffing + response.setHeader("Content-Type","picture/png"); // Try Browser to force sniffing. + response.write(getSniffableContent(request.queryString)); + return; +} diff --git a/dom/security/test/general/mochitest.ini b/dom/security/test/general/mochitest.ini index 8f592f834a60..9bac2397ccaf 100644 --- a/dom/security/test/general/mochitest.ini +++ b/dom/security/test/general/mochitest.ini @@ -2,6 +2,9 @@ support-files = file_contentpolicytype_targeted_link_iframe.sjs file_nosniff_testserver.sjs + file_nosniff_navigation.sjs + file_nosniff_navigation_mismatch.sjs + file_nosniff_navigation_garbage.sjs file_block_script_wrong_mime_server.sjs file_block_toplevel_data_navigation.html file_block_toplevel_data_navigation2.html @@ -24,6 +27,7 @@ support-files = [test_contentpolicytype_targeted_link_iframe.html] [test_nosniff.html] +[test_nosniff_navigation.html] [test_block_script_wrong_mime.html] [test_block_toplevel_data_navigation.html] skip-if = toolkit == 'android' # intermittent failure diff --git a/dom/security/test/general/test_nosniff_navigation.html b/dom/security/test/general/test_nosniff_navigation.html new file mode 100644 index 000000000000..74a421ac0681 --- /dev/null +++ b/dom/security/test/general/test_nosniff_navigation.html @@ -0,0 +1,65 @@ + + + + Bug 1428473 Support X-Content-Type-Options: nosniff when navigating + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + diff --git a/ipc/glue/BackgroundUtils.cpp b/ipc/glue/BackgroundUtils.cpp index 280bf974540b..3f31886f22de 100644 --- a/ipc/glue/BackgroundUtils.cpp +++ b/ipc/glue/BackgroundUtils.cpp @@ -584,6 +584,7 @@ nsresult LoadInfoToLoadInfoArgs(nsILoadInfo* aLoadInfo, aLoadInfo->GetServiceWorkerTaintingSynthesized(), aLoadInfo->GetDocumentHasUserInteracted(), aLoadInfo->GetDocumentHasLoaded(), cspNonce, + aLoadInfo->GetSkipContentSniffing(), aLoadInfo->GetIsFromProcessingFrameAttributes(), cookieSettingsArgs, aLoadInfo->GetRequestBlockingReason(), maybeCspToInheritInfo)); @@ -746,7 +747,7 @@ nsresult LoadInfoArgsToLoadInfo( loadInfoArgs.serviceWorkerTaintingSynthesized(), loadInfoArgs.documentHasUserInteracted(), loadInfoArgs.documentHasLoaded(), loadInfoArgs.cspNonce(), - loadInfoArgs.requestBlockingReason()); + loadInfoArgs.skipContentSniffing(), loadInfoArgs.requestBlockingReason()); if (loadInfoArgs.isFromProcessingFrameAttributes()) { loadInfo->SetIsFromProcessingFrameAttributes(); @@ -761,6 +762,7 @@ void LoadInfoToParentLoadInfoForwarder( if (!aLoadInfo) { *aForwarderArgsOut = ParentLoadInfoForwarderArgs( false, false, Nothing(), nsILoadInfo::TAINTING_BASIC, + false, // SkipContentSniffing false, // serviceWorkerTaintingSynthesized false, // documentHasUserInteracted false, // documentHasLoaded @@ -792,6 +794,7 @@ void LoadInfoToParentLoadInfoForwarder( *aForwarderArgsOut = ParentLoadInfoForwarderArgs( aLoadInfo->GetAllowInsecureRedirectToDataURI(), aLoadInfo->GetBypassCORSChecks(), ipcController, tainting, + aLoadInfo->GetSkipContentSniffing(), aLoadInfo->GetServiceWorkerTaintingSynthesized(), aLoadInfo->GetDocumentHasUserInteracted(), aLoadInfo->GetDocumentHasLoaded(), cookieSettingsArgs, @@ -826,6 +829,9 @@ nsresult MergeParentLoadInfoForwarder( aLoadInfo->MaybeIncreaseTainting(aForwarderArgs.tainting()); } + rv = aLoadInfo->SetSkipContentSniffing(aForwarderArgs.skipContentSniffing()); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ALWAYS_SUCCEEDS(aLoadInfo->SetDocumentHasUserInteracted( aForwarderArgs.documentHasUserInteracted())); MOZ_ALWAYS_SUCCEEDS( diff --git a/netwerk/base/LoadInfo.cpp b/netwerk/base/LoadInfo.cpp index 73876f7b5684..608c3878c8df 100644 --- a/netwerk/base/LoadInfo.cpp +++ b/netwerk/base/LoadInfo.cpp @@ -97,6 +97,7 @@ LoadInfo::LoadInfo( mServiceWorkerTaintingSynthesized(false), mDocumentHasUserInteracted(false), mDocumentHasLoaded(false), + mSkipContentSniffing(false), mIsFromProcessingFrameAttributes(false) { MOZ_ASSERT(mLoadingPrincipal); MOZ_ASSERT(mTriggeringPrincipal); @@ -358,6 +359,7 @@ LoadInfo::LoadInfo(nsPIDOMWindowOuter* aOuterWindow, mServiceWorkerTaintingSynthesized(false), mDocumentHasUserInteracted(false), mDocumentHasLoaded(false), + mSkipContentSniffing(false), mIsFromProcessingFrameAttributes(false) { // Top-level loads are never third-party // Grab the information we can out of the window. @@ -475,6 +477,7 @@ LoadInfo::LoadInfo(const LoadInfo& rhs) mDocumentHasUserInteracted(rhs.mDocumentHasUserInteracted), mDocumentHasLoaded(rhs.mDocumentHasLoaded), mCspNonce(rhs.mCspNonce), + mSkipContentSniffing(rhs.mSkipContentSniffing), mIsFromProcessingFrameAttributes(rhs.mIsFromProcessingFrameAttributes) {} LoadInfo::LoadInfo( @@ -508,7 +511,7 @@ LoadInfo::LoadInfo( bool aIsPreflight, bool aLoadTriggeredFromExternal, bool aServiceWorkerTaintingSynthesized, bool aDocumentHasUserInteracted, bool aDocumentHasLoaded, const nsAString& aCspNonce, - uint32_t aRequestBlockingReason) + bool aSkipContentSniffing, uint32_t aRequestBlockingReason) : mLoadingPrincipal(aLoadingPrincipal), mTriggeringPrincipal(aTriggeringPrincipal), mPrincipalToInherit(aPrincipalToInherit), @@ -559,6 +562,7 @@ LoadInfo::LoadInfo( mDocumentHasUserInteracted(aDocumentHasUserInteracted), mDocumentHasLoaded(aDocumentHasLoaded), mCspNonce(aCspNonce), + mSkipContentSniffing(aSkipContentSniffing), mIsFromProcessingFrameAttributes(false) { // Only top level TYPE_DOCUMENT loads can have a null loadingPrincipal MOZ_ASSERT(mLoadingPrincipal || @@ -1321,6 +1325,18 @@ LoadInfo::SetCspNonce(const nsAString& aCspNonce) { return NS_OK; } +NS_IMETHODIMP +LoadInfo::GetSkipContentSniffing(bool* aSkipContentSniffing) { + *aSkipContentSniffing = mSkipContentSniffing; + return NS_OK; +} + +NS_IMETHODIMP +LoadInfo::SetSkipContentSniffing(bool aSkipContentSniffing) { + mSkipContentSniffing = aSkipContentSniffing; + return NS_OK; +} + NS_IMETHODIMP LoadInfo::GetIsTopLevelLoad(bool* aResult) { *aResult = mFrameOuterWindowID ? mFrameOuterWindowID == mOuterWindowID diff --git a/netwerk/base/LoadInfo.h b/netwerk/base/LoadInfo.h index b179b52521db..9d896c2bfeb2 100644 --- a/netwerk/base/LoadInfo.h +++ b/netwerk/base/LoadInfo.h @@ -154,7 +154,8 @@ class LoadInfo final : public nsILoadInfo { bool aIsPreflight, bool aLoadTriggeredFromExternal, bool aServiceWorkerTaintingSynthesized, bool aDocumentHasUserInteracted, bool aDocumentHasLoaded, - const nsAString& aCspNonce, uint32_t aRequestBlockingReason); + const nsAString& aCspNonce, bool aSkipContentSniffing, + uint32_t aRequestBlockingReason); LoadInfo(const LoadInfo& rhs); NS_IMETHOD GetRedirects(JSContext* aCx, @@ -246,6 +247,7 @@ class LoadInfo final : public nsILoadInfo { bool mDocumentHasUserInteracted; bool mDocumentHasLoaded; nsString mCspNonce; + bool mSkipContentSniffing; // Is true if this load was triggered by processing the attributes of the // browsing context container. diff --git a/netwerk/base/nsILoadInfo.idl b/netwerk/base/nsILoadInfo.idl index 95030a3f98c2..e3ce61cf7b4d 100644 --- a/netwerk/base/nsILoadInfo.idl +++ b/netwerk/base/nsILoadInfo.idl @@ -413,6 +413,15 @@ interface nsILoadInfo : nsISupports */ [infallible] readonly attribute unsigned long securityMode; + + /** + * This flag is used for any browsing context where we should not sniff + * the content type. E.g if an iframe has the XCTO nosniff header, then + * that flag is set to true so we skip content sniffing for that browsing + * context. + */ + [infallible] attribute boolean skipContentSniffing; + /** * True if this request is embedded in a context that can't be third-party * (i.e. an iframe embedded in a cross-origin parent window). If this is diff --git a/netwerk/base/nsNetUtil.cpp b/netwerk/base/nsNetUtil.cpp index 0edcecd417c1..ff2951cff39f 100644 --- a/netwerk/base/nsNetUtil.cpp +++ b/netwerk/base/nsNetUtil.cpp @@ -2703,6 +2703,15 @@ nsresult NS_GenerateHostPort(const nsCString& host, int32_t port, void NS_SniffContent(const char* aSnifferType, nsIRequest* aRequest, const uint8_t* aData, uint32_t aLength, nsACString& aSniffedType) { + // In case XCTO nosniff was present, we could just skip sniffing here + nsCOMPtr channel = do_QueryInterface(aRequest); + if (channel) { + nsCOMPtr loadInfo = channel->LoadInfo(); + if (loadInfo->GetSkipContentSniffing()) { + aSniffedType.Truncate(); + return; + } + } typedef nsCategoryCache ContentSnifferCache; extern ContentSnifferCache* gNetSniffers; extern ContentSnifferCache* gDataSniffers; diff --git a/netwerk/ipc/NeckoChannelParams.ipdlh b/netwerk/ipc/NeckoChannelParams.ipdlh index 76e47805e7f7..4f7acc8377e9 100644 --- a/netwerk/ipc/NeckoChannelParams.ipdlh +++ b/netwerk/ipc/NeckoChannelParams.ipdlh @@ -139,6 +139,7 @@ struct LoadInfoArgs bool documentHasUserInteracted; bool documentHasLoaded; nsString cspNonce; + bool skipContentSniffing; bool isFromProcessingFrameAttributes; CookieSettingsArgs cookieSettings; uint32_t requestBlockingReason; @@ -172,6 +173,12 @@ struct ParentLoadInfoForwarderArgs // tainting value. uint32_t tainting; + + // This flag is used for any browsing context where we should not sniff + // the content type. E.g if an iframe has the XCTO nosniff header, then + // that flag is set to true so we skip content sniffing for that browsing + bool skipContentSniffing; + // We must also note that the tainting value was explicitly set // by the service worker. bool serviceWorkerTaintingSynthesized; diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp index 0c32788a7e02..08dd1c2471a2 100644 --- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -17,6 +17,7 @@ #include "nsHttpChannel.h" #include "nsHttpChannelAuthProvider.h" #include "nsHttpHandler.h" +#include "nsString.h" #include "nsIApplicationCacheService.h" #include "nsIApplicationCacheContainer.h" #include "nsICacheStorageService.h" @@ -1449,6 +1450,16 @@ nsresult ProcessXCTO(nsHttpChannel* aChannel, nsIURI* aURI, Report::Error); return NS_ERROR_CORRUPTED_CONTENT; } + auto policyType = aLoadInfo->GetExternalContentPolicyType(); + if (policyType == nsIContentPolicy::TYPE_DOCUMENT || + policyType == nsIContentPolicy::TYPE_SUBDOCUMENT) { + // If the header XCTO nosniff is set for any browsing context, then + // we set the skipContentSniffing flag on the Loadinfo. Within + // NS_SniffContent we then bail early and do not do any sniffing. + aLoadInfo->SetSkipContentSniffing(true); + return NS_OK; + } + return NS_OK; } diff --git a/netwerk/streamconv/converters/nsUnknownDecoder.cpp b/netwerk/streamconv/converters/nsUnknownDecoder.cpp index 313d63bfd55b..3615ede6aedc 100644 --- a/netwerk/streamconv/converters/nsUnknownDecoder.cpp +++ b/netwerk/streamconv/converters/nsUnknownDecoder.cpp @@ -326,6 +326,11 @@ nsUnknownDecoder::GetMIMETypeFromContent(nsIRequest* aRequest, nsACString& type) { // This is only used by sniffer, therefore we do not need to lock anything // here. + nsCOMPtr channel(do_QueryInterface(aRequest)); + nsCOMPtr loadInfo = channel->LoadInfo(); + if (loadInfo->GetSkipContentSniffing()) { + return NS_OK; + } mBuffer = const_cast(reinterpret_cast(aData)); mBufferLen = aLength; @@ -355,6 +360,11 @@ bool nsUnknownDecoder::AllowSniffing(nsIRequest* aRequest) { return false; } + nsCOMPtr loadInfo = channel->LoadInfo(); + if (loadInfo->GetSkipContentSniffing()) { + return false; + } + bool isLocalFile = false; if (NS_FAILED(uri->SchemeIs("file", &isLocalFile)) || isLocalFile) { return false; @@ -401,10 +411,17 @@ void nsUnknownDecoder::DetermineContentType(nsIRequest* aRequest) { if (!mContentType.IsEmpty()) return; } + nsCOMPtr channel(do_QueryInterface(aRequest)); + if (channel) { + nsCOMPtr loadInfo = channel->LoadInfo(); + if (loadInfo->GetSkipContentSniffing()) { + return; + } + } + const char* testData = mBuffer; uint32_t testDataLen = mBufferLen; // Check if data are compressed. - nsCOMPtr channel(do_QueryInterface(aRequest)); nsAutoCString decodedData; if (channel) { @@ -575,6 +592,11 @@ bool nsUnknownDecoder::SniffForXML(nsIRequest* aRequest) { } bool nsUnknownDecoder::SniffURI(nsIRequest* aRequest) { + nsCOMPtr channel(do_QueryInterface(aRequest)); + nsCOMPtr loadInfo = channel->LoadInfo(); + if (loadInfo->GetSkipContentSniffing()) { + return false; + } nsCOMPtr mimeService(do_GetService("@mozilla.org/mime;1")); if (mimeService) { nsCOMPtr channel = do_QueryInterface(aRequest); @@ -606,6 +628,12 @@ bool nsUnknownDecoder::LastDitchSniff(nsIRequest* aRequest) { // All we can do now is try to guess whether this is text/plain or // application/octet-stream + nsCOMPtr channel(do_QueryInterface(aRequest)); + nsCOMPtr loadInfo = channel->LoadInfo(); + if (loadInfo->GetSkipContentSniffing()) { + return false; + } + MutexAutoLock lock(mMutex); const char* testData; @@ -827,6 +855,10 @@ void nsBinaryDetector::DetermineContentType(nsIRequest* aRequest) { return; } + nsCOMPtr loadInfo = httpChannel->LoadInfo(); + if (loadInfo->GetSkipContentSniffing()) { + return; + } // It's an HTTP channel. Check for the text/plain mess nsAutoCString contentTypeHdr; Unused << httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("Content-Type"), diff --git a/parser/html/nsHtml5StreamParser.cpp b/parser/html/nsHtml5StreamParser.cpp index f0c0cae30c19..6604256fd3d4 100644 --- a/parser/html/nsHtml5StreamParser.cpp +++ b/parser/html/nsHtml5StreamParser.cpp @@ -190,7 +190,8 @@ nsHtml5StreamParser::nsHtml5StreamParser(nsHtml5TreeOpExecutor* aExecutor, mFlushTimerMutex("nsHtml5StreamParser mFlushTimerMutex"), mFlushTimerArmed(false), mFlushTimerEverFired(false), - mMode(aMode) { + mMode(aMode), + mSkipContentSniffing(false) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); #ifdef DEBUG mAtomTable.SetPermittedLookupEventTarget(mEventTarget); @@ -631,7 +632,7 @@ nsresult nsHtml5StreamParser::FinalizeSniffing(Span aFromSegment, } // meta scan failed. - if (mCharsetSource < kCharsetFromMetaPrescan) { + if (!mSkipContentSniffing && mCharsetSource < kCharsetFromMetaPrescan) { // Check for BOMless UTF-16 with Basic // Latin content for compat with IE. See bug 631751. SniffBOMlessUTF16BasicLatin(aFromSegment.To(aCountToSniffingLimit)); @@ -662,6 +663,7 @@ nsresult nsHtml5StreamParser::SniffStreamBytes( Span aFromSegment) { NS_ASSERTION(IsParserThread(), "Wrong thread!"); nsresult rv = NS_OK; + // mEncoding and mCharsetSource potentially have come from channel or higher // by now. If we find a BOM, SetupDecodingFromBom() will overwrite them. // If we don't find a BOM, the previously set values of mEncoding and @@ -957,6 +959,13 @@ nsresult nsHtml5StreamParser::OnStartRequest(nsIRequest* aRequest) { mObserver->OnStartRequest(aRequest); } mRequest = aRequest; + nsCOMPtr myChannel(do_QueryInterface(aRequest)); + nsCOMPtr loadInfo = myChannel->LoadInfo(); + mSkipContentSniffing = loadInfo->GetSkipContentSniffing(); + + if (mSkipContentSniffing) { + mFeedChardet = false; + } mStreamState = STREAM_BEING_READ; diff --git a/parser/html/nsHtml5StreamParser.h b/parser/html/nsHtml5StreamParser.h index 66f99a986894..babb274d8c8a 100644 --- a/parser/html/nsHtml5StreamParser.h +++ b/parser/html/nsHtml5StreamParser.h @@ -594,6 +594,11 @@ class nsHtml5StreamParser final : public nsICharsetDetectionObserver { * Whether the parser is doing a normal parse, view source or plain text. */ eParserMode mMode; + + /** + * Whether the parser should not sniff the content type. + */ + bool mSkipContentSniffing; }; #endif // nsHtml5StreamParser_h