diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /dom/security | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/security')
443 files changed, 31574 insertions, 0 deletions
diff --git a/dom/security/ContentVerifier.cpp b/dom/security/ContentVerifier.cpp new file mode 100644 index 000000000..2d3fadea8 --- /dev/null +++ b/dom/security/ContentVerifier.cpp @@ -0,0 +1,230 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ContentVerifier.h" + +#include "mozilla/fallible.h" +#include "mozilla/Logging.h" +#include "MainThreadUtils.h" +#include "nsIInputStream.h" +#include "nsIRequest.h" +#include "nsServiceManagerUtils.h" +#include "nsStringStream.h" + +using namespace mozilla; + +static LazyLogModule gContentVerifierPRLog("ContentVerifier"); +#define CSV_LOG(args) MOZ_LOG(gContentVerifierPRLog, LogLevel::Debug, args) + +NS_IMPL_ISUPPORTS(ContentVerifier, + nsIContentSignatureReceiverCallback, + nsIStreamListener); + +nsresult +ContentVerifier::Init(const nsACString& aContentSignatureHeader, + nsIRequest* aRequest, nsISupports* aContext) +{ + MOZ_ASSERT(NS_IsMainThread()); + if (aContentSignatureHeader.IsEmpty()) { + CSV_LOG(("Content-Signature header must not be empty!\n")); + return NS_ERROR_INVALID_SIGNATURE; + } + + // initialise the content signature "service" + nsresult rv; + mVerifier = + do_CreateInstance("@mozilla.org/security/contentsignatureverifier;1", &rv); + if (NS_FAILED(rv) || !mVerifier) { + return NS_ERROR_INVALID_SIGNATURE; + } + + // Keep references to the request and context. We need them in FinishSignature + // and the ContextCreated callback. + mContentRequest = aRequest; + mContentContext = aContext; + + rv = mVerifier->CreateContextWithoutCertChain( + this, aContentSignatureHeader, + NS_LITERAL_CSTRING("remotenewtab.content-signature.mozilla.org")); + if (NS_FAILED(rv)){ + mVerifier = nullptr; + } + return rv; +} + +/** + * Implement nsIStreamListener + * We buffer the entire content here and kick off verification + */ +nsresult +AppendNextSegment(nsIInputStream* aInputStream, void* aClosure, + const char* aRawSegment, uint32_t aToOffset, uint32_t aCount, + uint32_t* outWrittenCount) +{ + FallibleTArray<nsCString>* decodedData = + static_cast<FallibleTArray<nsCString>*>(aClosure); + nsDependentCSubstring segment(aRawSegment, aCount); + if (!decodedData->AppendElement(segment, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + *outWrittenCount = aCount; + return NS_OK; +} + +void +ContentVerifier::FinishSignature() +{ + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr<nsIStreamListener> nextListener; + nextListener.swap(mNextListener); + + // Verify the content: + // If this fails, we return an invalid signature error to load a fallback page. + // If everthing is good, we return a new stream to the next listener and kick + // that one off. + bool verified = false; + nsresult rv = NS_OK; + + // If the content signature check fails, stop the load + // and return a signature error. NSS resources are freed by the + // ContentSignatureVerifier on destruction. + if (NS_FAILED(mVerifier->End(&verified)) || !verified) { + CSV_LOG(("failed to verify content\n")); + (void)nextListener->OnStopRequest(mContentRequest, mContentContext, + NS_ERROR_INVALID_SIGNATURE); + return; + } + CSV_LOG(("Successfully verified content signature.\n")); + + // We emptied the input stream so we have to create a new one from mContent + // to hand it to the consuming listener. + uint64_t offset = 0; + for (uint32_t i = 0; i < mContent.Length(); ++i) { + nsCOMPtr<nsIInputStream> oInStr; + rv = NS_NewCStringInputStream(getter_AddRefs(oInStr), mContent[i]); + if (NS_FAILED(rv)) { + break; + } + // let the next listener know that there is data in oInStr + rv = nextListener->OnDataAvailable(mContentRequest, mContentContext, oInStr, + offset, mContent[i].Length()); + offset += mContent[i].Length(); + if (NS_FAILED(rv)) { + break; + } + } + + // propagate OnStopRequest and return + nextListener->OnStopRequest(mContentRequest, mContentContext, rv); +} + +NS_IMETHODIMP +ContentVerifier::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) +{ + MOZ_CRASH("This OnStartRequest should've never been called!"); + return NS_OK; +} + +NS_IMETHODIMP +ContentVerifier::OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, + nsresult aStatus) +{ + // If we don't have a next listener, we handed off this request already. + // Return, there's nothing to do here. + if (!mNextListener) { + return NS_OK; + } + + if (NS_FAILED(aStatus)) { + CSV_LOG(("Stream failed\n")); + nsCOMPtr<nsIStreamListener> nextListener; + nextListener.swap(mNextListener); + return nextListener->OnStopRequest(aRequest, aContext, aStatus); + } + + mContentRead = true; + + // If the ContentSignatureVerifier is initialised, finish the verification. + if (mContextCreated) { + FinishSignature(); + return aStatus; + } + + return NS_OK; +} + +NS_IMETHODIMP +ContentVerifier::OnDataAvailable(nsIRequest* aRequest, nsISupports* aContext, + nsIInputStream* aInputStream, uint64_t aOffset, + uint32_t aCount) +{ + // buffer the entire stream + uint32_t read; + nsresult rv = aInputStream->ReadSegments(AppendNextSegment, &mContent, aCount, + &read); + if (NS_FAILED(rv)) { + return rv; + } + + // Update the signature verifier if the context has been created. + if (mContextCreated) { + return mVerifier->Update(mContent.LastElement()); + } + + return NS_OK; +} + +NS_IMETHODIMP +ContentVerifier::ContextCreated(bool successful) +{ + MOZ_ASSERT(NS_IsMainThread()); + if (!successful) { + // If we don't have a next listener, the request has been handed off already. + if (!mNextListener) { + return NS_OK; + } + // Get local reference to mNextListener and null it to ensure that we don't + // call it twice. + nsCOMPtr<nsIStreamListener> nextListener; + nextListener.swap(mNextListener); + + // Make sure that OnStartRequest was called and we have a request. + MOZ_ASSERT(mContentRequest); + + // In this case something went wrong with the cert. Let's stop this load. + CSV_LOG(("failed to get a valid cert chain\n")); + if (mContentRequest && nextListener) { + mContentRequest->Cancel(NS_ERROR_INVALID_SIGNATURE); + nsresult rv = nextListener->OnStopRequest(mContentRequest, mContentContext, + NS_ERROR_INVALID_SIGNATURE); + mContentRequest = nullptr; + mContentContext = nullptr; + return rv; + } + + // We should never get here! + MOZ_ASSERT_UNREACHABLE( + "ContentVerifier was used without getting OnStartRequest!"); + return NS_OK; + } + + // In this case the content verifier is initialised and we have to feed it + // the buffered content. + mContextCreated = true; + for (size_t i = 0; i < mContent.Length(); ++i) { + if (NS_FAILED(mVerifier->Update(mContent[i]))) { + // Bail out if this fails. We can't return an error here, but if this + // failed, NS_ERROR_INVALID_SIGNATURE is returned in FinishSignature. + break; + } + } + + // We read all content, let's verify the signature. + if (mContentRead) { + FinishSignature(); + } + + return NS_OK; +} diff --git a/dom/security/ContentVerifier.h b/dom/security/ContentVerifier.h new file mode 100644 index 000000000..e0c940197 --- /dev/null +++ b/dom/security/ContentVerifier.h @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ContentVerifier_h +#define mozilla_dom_ContentVerifier_h + +#include "nsCOMPtr.h" +#include "nsIContentSignatureVerifier.h" +#include "nsIObserver.h" +#include "nsIStreamListener.h" +#include "nsString.h" +#include "nsTArray.h" + +/** + * Mediator intercepting OnStartRequest in nsHttpChannel, blocks until all + * data is read from the input stream, verifies the content signature and + * releases the request to the next listener if the verification is successful. + * If the verification fails or anything else goes wrong, a + * NS_ERROR_INVALID_SIGNATURE is thrown. + */ +class ContentVerifier : public nsIStreamListener + , public nsIContentSignatureReceiverCallback +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSICONTENTSIGNATURERECEIVERCALLBACK + + explicit ContentVerifier(nsIStreamListener* aMediatedListener, + nsISupports* aMediatedContext) + : mNextListener(aMediatedListener) + , mContextCreated(false) + , mContentRead(false) {} + + nsresult Init(const nsACString& aContentSignatureHeader, nsIRequest* aRequest, + nsISupports* aContext); + +protected: + virtual ~ContentVerifier() {} + +private: + void FinishSignature(); + + // buffered content to verify + FallibleTArray<nsCString> mContent; + // content and next listener for nsIStreamListener + nsCOMPtr<nsIStreamListener> mNextListener; + // the verifier + nsCOMPtr<nsIContentSignatureVerifier> mVerifier; + // holding a pointer to the content request and context to resume/cancel it + nsCOMPtr<nsIRequest> mContentRequest; + nsCOMPtr<nsISupports> mContentContext; + // Semaphors to indicate that the verifying context was created, the entire + // content was read resp. The context gets created by ContentSignatureVerifier + // and mContextCreated is set in the ContextCreated callback. The content is + // read, i.e. mContentRead is set, when the content OnStopRequest is called. + bool mContextCreated; + bool mContentRead; +}; + +#endif /* mozilla_dom_ContentVerifier_h */ diff --git a/dom/security/SRICheck.cpp b/dom/security/SRICheck.cpp new file mode 100644 index 000000000..534909f81 --- /dev/null +++ b/dom/security/SRICheck.cpp @@ -0,0 +1,543 @@ +/* -*- 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/LoadTainting.h" +#include "mozilla/Logging.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/SRILogHelper.h" +#include "mozilla/dom/SRIMetadata.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsIConsoleReportCollector.h" +#include "nsIProtocolHandler.h" +#include "nsIScriptError.h" +#include "nsIIncrementalStreamLoader.h" +#include "nsIUnicharStreamLoader.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsWhitespaceTokenizer.h" + +#define SRIVERBOSE(args) \ + MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Verbose, args) +#define SRILOG(args) \ + MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug, args) +#define SRIERROR(args) \ + MOZ_LOG(SRILogHelper::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, mozilla::LoadTainting aTainting, + const nsACString& aSourceFileURI, + nsIConsoleReportCollector* aReporter) +{ + NS_ENSURE_ARG_POINTER(aReporter); + + if (!aChannel) { + SRILOG(("SRICheck::IsEligible, null channel")); + return NS_ERROR_SRI_NOT_ELIGIBLE; + } + + // Was the sub-resource loaded via CORS? + if (aTainting == LoadTainting::CORS) { + SRILOG(("SRICheck::IsEligible, CORS mode")); + return NS_OK; + } + + nsCOMPtr<nsIURI> finalURI; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIURI> 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(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug)) { + SRILOG(("SRICheck::IsEligible, requestURI=%s; finalURI=%s", + requestSpec.get(), + finalURI ? finalURI->GetSpecOrDefault().get() : "")); + } + + // Is the sub-resource same-origin? + if (aTainting == LoadTainting::Basic) { + SRILOG(("SRICheck::IsEligible, same-origin")); + return NS_OK; + } + SRILOG(("SRICheck::IsEligible, NOT same origin")); + + NS_ConvertUTF8toUTF16 requestSpecUTF16(requestSpec); + nsTArray<nsString> params; + params.AppendElement(requestSpecUTF16); + aReporter->AddConsoleReport(nsIScriptError::errorFlag, + NS_LITERAL_CSTRING("Sub-resource Integrity"), + nsContentUtils::eSECURITY_PROPERTIES, + aSourceFileURI, 0, 0, + NS_LITERAL_CSTRING("IneligibleResource"), + const_cast<const nsTArray<nsString>&>(params)); + return NS_ERROR_SRI_NOT_ELIGIBLE; +} + +/* static */ nsresult +SRICheck::IntegrityMetadata(const nsAString& aMetadataList, + const nsACString& aSourceFileURI, + nsIConsoleReportCollector* aReporter, + SRIMetadata* outMetadata) +{ + NS_ENSURE_ARG_POINTER(outMetadata); + NS_ENSURE_ARG_POINTER(aReporter); + 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); + nsTArray<nsString> params; + params.AppendElement(tokenUTF16); + aReporter->AddConsoleReport(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("Sub-resource Integrity"), + nsContentUtils::eSECURITY_PROPERTIES, + aSourceFileURI, 0, 0, + NS_LITERAL_CSTRING("MalformedIntegrityHash"), + const_cast<const nsTArray<nsString>&>(params)); + } else if (!metadata.IsAlgorithmSupported()) { + nsAutoCString alg; + metadata.GetAlgorithm(&alg); + NS_ConvertUTF8toUTF16 algUTF16(alg); + nsTArray<nsString> params; + params.AppendElement(algUTF16); + aReporter->AddConsoleReport(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("Sub-resource Integrity"), + nsContentUtils::eSECURITY_PROPERTIES, + aSourceFileURI, 0, 0, + NS_LITERAL_CSTRING("UnsupportedHashAlg"), + const_cast<const nsTArray<nsString>&>(params)); + } + + nsAutoCString alg1, alg2; + if (MOZ_LOG_TEST(SRILogHelper::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 + } + } + + outMetadata->mIntegrityString = aMetadataList; + + if (MOZ_LOG_TEST(SRILogHelper::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, + nsIUnicharStreamLoader* aLoader, + const nsAString& aString, + const nsACString& aSourceFileURI, + nsIConsoleReportCollector* aReporter) +{ + NS_ENSURE_ARG_POINTER(aLoader); + NS_ENSURE_ARG_POINTER(aReporter); + + nsCOMPtr<nsIChannel> channel; + aLoader->GetChannel(getter_AddRefs(channel)); + + if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug)) { + nsAutoCString requestURL; + nsCOMPtr<nsIURI> originalURI; + if (channel && + NS_SUCCEEDED(channel->GetOriginalURI(getter_AddRefs(originalURI))) && + originalURI) { + originalURI->GetAsciiSpec(requestURL); + } + SRILOG(("SRICheck::VerifyIntegrity (unichar stream)")); + } + + SRICheckDataVerifier verifier(aMetadata, aSourceFileURI, aReporter); + nsresult rv; + nsDependentCString rawBuffer; + rv = aLoader->GetRawBuffer(rawBuffer); + NS_ENSURE_SUCCESS(rv, rv); + rv = verifier.Update(rawBuffer.Length(), (const uint8_t*)rawBuffer.get()); + NS_ENSURE_SUCCESS(rv, rv); + + return verifier.Verify(aMetadata, channel, aSourceFileURI, aReporter); +} + +////////////////////////////////////////////////////////////// +// +////////////////////////////////////////////////////////////// +SRICheckDataVerifier::SRICheckDataVerifier(const SRIMetadata& aMetadata, + const nsACString& aSourceFileURI, + nsIConsoleReportCollector* aReporter) + : mCryptoHash(nullptr), + mBytesHashed(0), + mInvalidMetadata(false), + mComplete(false) +{ + 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)); + MOZ_ASSERT(aReporter); + + if (!aMetadata.IsValid()) { + nsTArray<nsString> params; + aReporter->AddConsoleReport(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("Sub-resource Integrity"), + nsContentUtils::eSECURITY_PROPERTIES, + aSourceFileURI, 0, 0, + NS_LITERAL_CSTRING("NoValidMetadata"), + const_cast<const nsTArray<nsString>&>(params)); + mInvalidMetadata = true; + return; // ignore invalid metadata for forward-compatibility + } + + aMetadata.GetHashType(&mHashType, &mHashLength); +} + +nsresult +SRICheckDataVerifier::EnsureCryptoHash() +{ + MOZ_ASSERT(!mInvalidMetadata); + + if (mCryptoHash) { + return NS_OK; + } + + nsresult rv; + nsCOMPtr<nsICryptoHash> cryptoHash = + do_CreateInstance("@mozilla.org/security/hash;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = cryptoHash->Init(mHashType); + NS_ENSURE_SUCCESS(rv, rv); + + mCryptoHash = cryptoHash; + return NS_OK; +} + +nsresult +SRICheckDataVerifier::Update(uint32_t aStringLen, const uint8_t* aString) +{ + NS_ENSURE_ARG_POINTER(aString); + if (mInvalidMetadata) { + return NS_OK; // ignoring any data updates, see mInvalidMetadata usage + } + + nsresult rv; + rv = EnsureCryptoHash(); + NS_ENSURE_SUCCESS(rv, rv); + + mBytesHashed += aStringLen; + + return mCryptoHash->Update(aString, aStringLen); +} + +nsresult +SRICheckDataVerifier::Finish() +{ + if (mInvalidMetadata || mComplete) { + return NS_OK; // already finished or invalid metadata + } + + nsresult rv; + rv = EnsureCryptoHash(); // we need computed hash even for 0-length data + NS_ENSURE_SUCCESS(rv, rv); + + rv = mCryptoHash->Finish(false, mComputedHash); + mCryptoHash = nullptr; + mComplete = true; + return rv; +} + +nsresult +SRICheckDataVerifier::VerifyHash(const SRIMetadata& aMetadata, + uint32_t aHashIndex, + const nsACString& aSourceFileURI, + nsIConsoleReportCollector* aReporter) +{ + NS_ENSURE_ARG_POINTER(aReporter); + + nsAutoCString base64Hash; + aMetadata.GetHash(aHashIndex, &base64Hash); + SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u]=%s", aHashIndex, base64Hash.get())); + + nsAutoCString binaryHash; + if (NS_WARN_IF(NS_FAILED(Base64Decode(base64Hash, binaryHash)))) { + nsTArray<nsString> params; + aReporter->AddConsoleReport(nsIScriptError::errorFlag, + NS_LITERAL_CSTRING("Sub-resource Integrity"), + nsContentUtils::eSECURITY_PROPERTIES, + aSourceFileURI, 0, 0, + NS_LITERAL_CSTRING("InvalidIntegrityBase64"), + const_cast<const nsTArray<nsString>&>(params)); + return NS_ERROR_SRI_CORRUPT; + } + + uint32_t hashLength; + int8_t hashType; + aMetadata.GetHashType(&hashType, &hashLength); + if (binaryHash.Length() != hashLength) { + nsTArray<nsString> params; + aReporter->AddConsoleReport(nsIScriptError::errorFlag, + NS_LITERAL_CSTRING("Sub-resource Integrity"), + nsContentUtils::eSECURITY_PROPERTIES, + aSourceFileURI, 0, 0, + NS_LITERAL_CSTRING("InvalidIntegrityLength"), + const_cast<const nsTArray<nsString>&>(params)); + return NS_ERROR_SRI_CORRUPT; + } + + if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug)) { + nsAutoCString encodedHash; + nsresult rv = Base64Encode(mComputedHash, encodedHash); + if (NS_SUCCEEDED(rv)) { + SRILOG(("SRICheckDataVerifier::VerifyHash, mComputedHash=%s", + encodedHash.get())); + } + } + + if (!binaryHash.Equals(mComputedHash)) { + SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u] did not match", aHashIndex)); + return NS_ERROR_SRI_CORRUPT; + } + + SRILOG(("SRICheckDataVerifier::VerifyHash, hash[%u] verified successfully", aHashIndex)); + return NS_OK; +} + +nsresult +SRICheckDataVerifier::Verify(const SRIMetadata& aMetadata, + nsIChannel* aChannel, + const nsACString& aSourceFileURI, + nsIConsoleReportCollector* aReporter) +{ + NS_ENSURE_ARG_POINTER(aReporter); + + if (MOZ_LOG_TEST(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug)) { + nsAutoCString requestURL; + nsCOMPtr<nsIRequest> request; + request = do_QueryInterface(aChannel); + request->GetName(requestURL); + SRILOG(("SRICheckDataVerifier::Verify, url=%s (length=%lu)", + requestURL.get(), mBytesHashed)); + } + + nsresult rv = Finish(); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo(); + NS_ENSURE_TRUE(loadInfo, NS_ERROR_FAILURE); + LoadTainting tainting = loadInfo->GetTainting(); + + if (NS_FAILED(IsEligible(aChannel, tainting, aSourceFileURI, aReporter))) { + return NS_ERROR_SRI_NOT_ELIGIBLE; + } + + if (mInvalidMetadata) { + return NS_OK; // ignore invalid metadata for forward-compatibility + } + + for (uint32_t i = 0; i < aMetadata.HashCount(); i++) { + if (NS_SUCCEEDED(VerifyHash(aMetadata, i, aSourceFileURI, aReporter))) { + return NS_OK; // stop at the first valid hash + } + } + + nsAutoCString alg; + aMetadata.GetAlgorithm(&alg); + NS_ConvertUTF8toUTF16 algUTF16(alg); + nsTArray<nsString> params; + params.AppendElement(algUTF16); + aReporter->AddConsoleReport(nsIScriptError::errorFlag, + NS_LITERAL_CSTRING("Sub-resource Integrity"), + nsContentUtils::eSECURITY_PROPERTIES, + aSourceFileURI, 0, 0, + NS_LITERAL_CSTRING("IntegrityMismatch"), + const_cast<const nsTArray<nsString>&>(params)); + return NS_ERROR_SRI_CORRUPT; +} + +uint32_t +SRICheckDataVerifier::DataSummaryLength() +{ + MOZ_ASSERT(!mInvalidMetadata); + return sizeof(mHashType) + sizeof(mHashLength) + mHashLength; +} + +uint32_t +SRICheckDataVerifier::EmptyDataSummaryLength() +{ + return sizeof(int8_t) + sizeof(uint32_t); +} + +nsresult +SRICheckDataVerifier::DataSummaryLength(uint32_t aDataLen, const uint8_t* aData, uint32_t* length) +{ + *length = 0; + NS_ENSURE_ARG_POINTER(aData); + + // we expect to always encode an SRI, even if it is empty or incomplete + if (aDataLen < EmptyDataSummaryLength()) { + SRILOG(("SRICheckDataVerifier::DataSummaryLength, encoded length[%u] is too small", aDataLen)); + return NS_ERROR_SRI_IMPORT; + } + + // decode the content of the buffer + size_t offset = sizeof(mHashType); + size_t len = *reinterpret_cast<const decltype(mHashLength)*>(&aData[offset]); + offset += sizeof(mHashLength); + + SRIVERBOSE(("SRICheckDataVerifier::DataSummaryLength, header {%x, %x, %x, %x, %x, ...}", + aData[0], aData[1], aData[2], aData[3], aData[4])); + + if (offset + len > aDataLen) { + SRILOG(("SRICheckDataVerifier::DataSummaryLength, encoded length[%u] overflow the buffer size", aDataLen)); + SRIVERBOSE(("SRICheckDataVerifier::DataSummaryLength, offset[%u], len[%u]", + uint32_t(offset), uint32_t(len))); + return NS_ERROR_SRI_IMPORT; + } + *length = uint32_t(offset + len); + return NS_OK; +} + +nsresult +SRICheckDataVerifier::ImportDataSummary(uint32_t aDataLen, const uint8_t* aData) +{ + MOZ_ASSERT(!mInvalidMetadata); // mHashType and mHashLength should be valid + MOZ_ASSERT(!mCryptoHash); // EnsureCryptoHash should not have been called + NS_ENSURE_ARG_POINTER(aData); + if (mInvalidMetadata) { + return NS_OK; // ignoring any data updates, see mInvalidMetadata usage + } + + // we expect to always encode an SRI, even if it is empty or incomplete + if (aDataLen < DataSummaryLength()) { + SRILOG(("SRICheckDataVerifier::ImportDataSummary, encoded length[%u] is too small", aDataLen)); + return NS_ERROR_SRI_IMPORT; + } + + SRIVERBOSE(("SRICheckDataVerifier::ImportDataSummary, header {%x, %x, %x, %x, %x, ...}", + aData[0], aData[1], aData[2], aData[3], aData[4])); + + // decode the content of the buffer + size_t offset = 0; + if (*reinterpret_cast<const decltype(mHashType)*>(&aData[offset]) != mHashType) { + SRILOG(("SRICheckDataVerifier::ImportDataSummary, hash type[%d] does not match[%d]", + *reinterpret_cast<const decltype(mHashType)*>(&aData[offset]), + mHashType)); + return NS_ERROR_SRI_UNEXPECTED_HASH_TYPE; + } + offset += sizeof(mHashType); + + if (*reinterpret_cast<const decltype(mHashLength)*>(&aData[offset]) != mHashLength) { + SRILOG(("SRICheckDataVerifier::ImportDataSummary, hash length[%d] does not match[%d]", + *reinterpret_cast<const decltype(mHashLength)*>(&aData[offset]), + mHashLength)); + return NS_ERROR_SRI_UNEXPECTED_HASH_TYPE; + } + offset += sizeof(mHashLength); + + // copy the hash to mComputedHash, as-if we had finished streaming the bytes + mComputedHash.Assign(reinterpret_cast<const char*>(&aData[offset]), mHashLength); + mCryptoHash = nullptr; + mComplete = true; + return NS_OK; +} + +nsresult +SRICheckDataVerifier::ExportDataSummary(uint32_t aDataLen, uint8_t* aData) +{ + MOZ_ASSERT(!mInvalidMetadata); // mHashType and mHashLength should be valid + MOZ_ASSERT(mComplete); // finished streaming + NS_ENSURE_ARG_POINTER(aData); + NS_ENSURE_TRUE(aDataLen >= DataSummaryLength(), NS_ERROR_INVALID_ARG); + + // serialize the hash in the buffer + size_t offset = 0; + *reinterpret_cast<decltype(mHashType)*>(&aData[offset]) = mHashType; + offset += sizeof(mHashType); + *reinterpret_cast<decltype(mHashLength)*>(&aData[offset]) = mHashLength; + offset += sizeof(mHashLength); + + SRIVERBOSE(("SRICheckDataVerifier::ExportDataSummary, header {%x, %x, %x, %x, %x, ...}", + aData[0], aData[1], aData[2], aData[3], aData[4])); + + // copy the hash to mComputedHash, as-if we had finished streaming the bytes + nsCharTraits<char>::copy(reinterpret_cast<char*>(&aData[offset]), + mComputedHash.get(), mHashLength); + return NS_OK; +} + +nsresult +SRICheckDataVerifier::ExportEmptyDataSummary(uint32_t aDataLen, uint8_t* aData) +{ + NS_ENSURE_ARG_POINTER(aData); + NS_ENSURE_TRUE(aDataLen >= EmptyDataSummaryLength(), NS_ERROR_INVALID_ARG); + + // serialize an unknown hash in the buffer, to be able to skip it later + size_t offset = 0; + *reinterpret_cast<decltype(mHashType)*>(&aData[offset]) = 0; + offset += sizeof(mHashType); + *reinterpret_cast<decltype(mHashLength)*>(&aData[offset]) = 0; + offset += sizeof(mHashLength); + + SRIVERBOSE(("SRICheckDataVerifier::ExportEmptyDataSummary, header {%x, %x, %x, %x, %x, ...}", + aData[0], aData[1], aData[2], aData[3], aData[4])); + + return NS_OK; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/security/SRICheck.h b/dom/security/SRICheck.h new file mode 100644 index 000000000..82929fe36 --- /dev/null +++ b/dom/security/SRICheck.h @@ -0,0 +1,119 @@ +/* -*- 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 "nsCOMPtr.h" +#include "nsICryptoHash.h" + +class nsIChannel; +class nsIUnicharStreamLoader; +class nsIConsoleReportCollector; + +namespace mozilla { +namespace dom { + +class SRIMetadata; + +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 nsACString& aSourceFileURI, + nsIConsoleReportCollector* aReporter, + 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 nsAString& aString, + const nsACString& aSourceFileURI, + nsIConsoleReportCollector* aReporter); +}; + +// The SRICheckDataVerifier can be used in 2 different mode: +// +// 1. The streaming mode involves reading bytes from an input, and to use +// the |Update| function to stream new bytes, and to use the |Verify| +// function to check the hash of the content with the hash provided by +// the metadata. +// +// Optionally, one can serialize the verified hash with |ExportDataSummary|, +// in a buffer in order to rely on the second mode the next time. +// +// 2. The pre-computed mode, involves reading a hash with |ImportDataSummary|, +// which got exported by the SRICheckDataVerifier and potentially cached, and +// then use the |Verify| function to check against the hash provided by the +// metadata. +class SRICheckDataVerifier final +{ + public: + SRICheckDataVerifier(const SRIMetadata& aMetadata, + const nsACString& aSourceFileURI, + nsIConsoleReportCollector* aReporter); + + // Append the following bytes to the content used to compute the hash. Once + // all bytes are streamed, use the Verify function to check the integrity. + nsresult Update(uint32_t aStringLen, const uint8_t* aString); + + // Verify that the computed hash corresponds to the metadata. + nsresult Verify(const SRIMetadata& aMetadata, nsIChannel* aChannel, + const nsACString& aSourceFileURI, + nsIConsoleReportCollector* aReporter); + + bool IsComplete() const { + return mComplete; + } + + // Report the length of the computed hash and its type, such that we can + // reserve the space for encoding it in a vector. + uint32_t DataSummaryLength(); + static uint32_t EmptyDataSummaryLength(); + + // Write the computed hash and its type in a pre-allocated buffer. + nsresult ExportDataSummary(uint32_t aDataLen, uint8_t* aData); + static nsresult ExportEmptyDataSummary(uint32_t aDataLen, uint8_t* aData); + + // Report the length of the computed hash and its type, such that we can + // skip these data while reading a buffer. + static nsresult DataSummaryLength(uint32_t aDataLen, const uint8_t* aData, uint32_t* length); + + // Extract the computed hash and its type, such that we can |Verify| if it + // matches the metadata. The buffer should be at least the same size or + // larger than the value returned by |DataSummaryLength|. + nsresult ImportDataSummary(uint32_t aDataLen, const uint8_t* aData); + + private: + nsCOMPtr<nsICryptoHash> mCryptoHash; + nsAutoCString mComputedHash; + size_t mBytesHashed; + uint32_t mHashLength; + int8_t mHashType; + bool mInvalidMetadata; + bool mComplete; + + nsresult EnsureCryptoHash(); + nsresult Finish(); + nsresult VerifyHash(const SRIMetadata& aMetadata, uint32_t aHashIndex, + const nsACString& aSourceFileURI, + nsIConsoleReportCollector* aReporter); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_SRICheck_h diff --git a/dom/security/SRILogHelper.h b/dom/security/SRILogHelper.h new file mode 100644 index 000000000..90638136d --- /dev/null +++ b/dom/security/SRILogHelper.h @@ -0,0 +1,28 @@ +/* -*- 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_SRILogHelper_h +#define mozilla_dom_SRILogHelper_h + +#include "mozilla/Logging.h" + +namespace mozilla { +namespace dom { + +class SRILogHelper final +{ +public: + static LogModule* GetSriLog() + { + static LazyLogModule gSriPRLog("SRI"); + return gSriPRLog; + } +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_SRILogHelper_h diff --git a/dom/security/SRIMetadata.cpp b/dom/security/SRIMetadata.cpp new file mode 100644 index 000000000..801ff0477 --- /dev/null +++ b/dom/security/SRIMetadata.cpp @@ -0,0 +1,169 @@ +/* -*- 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 mozilla::LogModule* +GetSriMetadataLog() +{ + static mozilla::LazyLogModule gSriMetadataPRLog("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<uint32_t>(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 000000000..4b6bdb47a --- /dev/null +++ b/dom/security/SRIMetadata.h @@ -0,0 +1,83 @@ +/* -*- 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" +#include "SRICheck.h" + +namespace mozilla { +namespace dom { + +class SRIMetadata final +{ + friend class SRICheck; + +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; + + const nsString& GetIntegrityString() const + { + return mIntegrityString; + } + +private: + nsTArray<nsCString> mHashes; + nsString mIntegrityString; + 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 new file mode 100644 index 000000000..00f7376a8 --- /dev/null +++ b/dom/security/moz.build @@ -0,0 +1,48 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +TEST_DIRS += ['test'] + +EXPORTS.mozilla.dom += [ + 'ContentVerifier.h', + 'nsContentSecurityManager.h', + 'nsCSPContext.h', + 'nsCSPService.h', + 'nsCSPUtils.h', + 'nsMixedContentBlocker.h', + 'SRICheck.h', + 'SRILogHelper.h', + 'SRIMetadata.h', +] + +EXPORTS += [ + 'nsContentSecurityManager.h', + 'nsMixedContentBlocker.h', +] + +UNIFIED_SOURCES += [ + 'ContentVerifier.cpp', + 'nsContentSecurityManager.cpp', + 'nsCSPContext.cpp', + 'nsCSPParser.cpp', + 'nsCSPService.cpp', + 'nsCSPUtils.cpp', + 'nsMixedContentBlocker.cpp', + 'SRICheck.cpp', + 'SRIMetadata.cpp', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' +LOCAL_INCLUDES += [ + '/caps', + '/netwerk/base', +] + +if CONFIG['GNU_CC']: + CFLAGS += ['-Wformat-security'] + CXXFLAGS += ['-Wformat-security'] diff --git a/dom/security/nsCSPContext.cpp b/dom/security/nsCSPContext.cpp new file mode 100644 index 000000000..815c7734d --- /dev/null +++ b/dom/security/nsCSPContext.cpp @@ -0,0 +1,1567 @@ +/* -*- 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 "nsCOMPtr.h" +#include "nsContentPolicyUtils.h" +#include "nsContentUtils.h" +#include "nsCSPContext.h" +#include "nsCSPParser.h" +#include "nsCSPService.h" +#include "nsError.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIClassInfoImpl.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeItem.h" +#include "nsIDOMHTMLDocument.h" +#include "nsIDOMHTMLElement.h" +#include "nsIDOMNode.h" +#include "nsIHttpChannel.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIStringStream.h" +#include "nsIUploadChannel.h" +#include "nsIScriptError.h" +#include "nsIWebNavigation.h" +#include "nsMimeTypes.h" +#include "nsNetUtil.h" +#include "nsIContentPolicy.h" +#include "nsSupportsPrimitives.h" +#include "nsThreadUtils.h" +#include "nsString.h" +#include "nsScriptSecurityManager.h" +#include "nsStringStream.h" +#include "mozilla/Logging.h" +#include "mozilla/dom/CSPReportBinding.h" +#include "mozilla/dom/CSPDictionariesBinding.h" +#include "mozilla/net/ReferrerPolicy.h" +#include "nsINetworkInterceptController.h" +#include "nsSandboxFlags.h" +#include "nsIScriptElement.h" + +using namespace mozilla; + +static LogModule* +GetCspContextLog() +{ + static LazyLogModule gCspContextPRLog("CSPContext"); + return gCspContextPRLog; +} + +#define CSPCONTEXTLOG(args) MOZ_LOG(GetCspContextLog(), mozilla::LogLevel::Debug, args) +#define CSPCONTEXTLOGENABLED() MOZ_LOG_TEST(GetCspContextLog(), mozilla::LogLevel::Debug) + +static const uint32_t CSP_CACHE_URI_CUTOFF_SIZE = 512; + +/** + * Creates a key for use in the ShouldLoad cache. + * Looks like: <uri>!<nsIContentPolicy::LOAD_TYPE> + */ +nsresult +CreateCacheKey_Internal(nsIURI* aContentLocation, + nsContentPolicyType aContentType, + nsACString& outCacheKey) +{ + if (!aContentLocation) { + return NS_ERROR_FAILURE; + } + + bool isDataScheme = false; + nsresult rv = aContentLocation->SchemeIs("data", &isDataScheme); + NS_ENSURE_SUCCESS(rv, rv); + + outCacheKey.Truncate(); + if (aContentType != nsIContentPolicy::TYPE_SCRIPT && isDataScheme) { + // For non-script data: URI, use ("data:", aContentType) as the cache key. + outCacheKey.Append(NS_LITERAL_CSTRING("data:")); + outCacheKey.AppendInt(aContentType); + return NS_OK; + } + + nsAutoCString spec; + rv = aContentLocation->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + // Don't cache for a URI longer than the cutoff size. + if (spec.Length() <= CSP_CACHE_URI_CUTOFF_SIZE) { + outCacheKey.Append(spec); + outCacheKey.Append(NS_LITERAL_CSTRING("!")); + outCacheKey.AppendInt(aContentType); + } + + return NS_OK; +} + +/* ===== nsIContentSecurityPolicy impl ====== */ + +NS_IMETHODIMP +nsCSPContext::ShouldLoad(nsContentPolicyType aContentType, + nsIURI* aContentLocation, + nsIURI* aRequestOrigin, + nsISupports* aRequestContext, + const nsACString& aMimeTypeGuess, + nsISupports* aExtra, + int16_t* outDecision) +{ + if (CSPCONTEXTLOGENABLED()) { + CSPCONTEXTLOG(("nsCSPContext::ShouldLoad, aContentLocation: %s", + aContentLocation->GetSpecOrDefault().get())); + CSPCONTEXTLOG((">>>> aContentType: %d", aContentType)); + } + + bool isPreload = nsContentUtils::IsPreloadType(aContentType); + + // Since we know whether we are dealing with a preload, we have to convert + // the internal policytype ot the external policy type before moving on. + // We still need to know if this is a worker so child-src can handle that + // case correctly. + aContentType = nsContentUtils::InternalContentPolicyTypeToExternalOrWorker(aContentType); + + nsresult rv = NS_OK; + + // This ShouldLoad function is called from nsCSPService::ShouldLoad, + // which already checked a number of things, including: + // * aContentLocation is not null; we can consume this without further checks + // * scheme is not a whitelisted scheme (about: chrome:, etc). + // * CSP is enabled + // * Content Type is not whitelisted (CSP Reports, TYPE_DOCUMENT, etc). + // * Fast Path for Apps + + nsAutoCString cacheKey; + rv = CreateCacheKey_Internal(aContentLocation, aContentType, cacheKey); + NS_ENSURE_SUCCESS(rv, rv); + + bool isCached = mShouldLoadCache.Get(cacheKey, outDecision); + if (isCached && cacheKey.Length() > 0) { + // this is cached, use the cached value. + return NS_OK; + } + + // Default decision, CSP can revise it if there's a policy to enforce + *outDecision = nsIContentPolicy::ACCEPT; + + // If the content type doesn't map to a CSP directive, there's nothing for + // CSP to do. + CSPDirective dir = CSP_ContentTypeToDirective(aContentType); + if (dir == nsIContentSecurityPolicy::NO_DIRECTIVE) { + return NS_OK; + } + + nsAutoString nonce; + bool parserCreated = false; + if (!isPreload) { + nsCOMPtr<nsIDOMHTMLElement> htmlElement = do_QueryInterface(aRequestContext); + if (htmlElement) { + rv = htmlElement->GetAttribute(NS_LITERAL_STRING("nonce"), nonce); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIScriptElement> script = do_QueryInterface(aRequestContext); + if (script && script->GetParserCreated() != mozilla::dom::NOT_FROM_PARSER) { + parserCreated = true; + } + } + + // aExtra is only non-null if the channel got redirected. + bool wasRedirected = (aExtra != nullptr); + nsCOMPtr<nsIURI> originalURI = do_QueryInterface(aExtra); + + bool permitted = permitsInternal(dir, + aContentLocation, + originalURI, + nonce, + wasRedirected, + isPreload, + false, // allow fallback to default-src + true, // send violation reports + true, // send blocked URI in violation reports + parserCreated); + + *outDecision = permitted ? nsIContentPolicy::ACCEPT + : nsIContentPolicy::REJECT_SERVER; + + // Done looping, cache any relevant result + if (cacheKey.Length() > 0 && !isPreload) { + mShouldLoadCache.Put(cacheKey, *outDecision); + } + + if (CSPCONTEXTLOGENABLED()) { + CSPCONTEXTLOG(("nsCSPContext::ShouldLoad, decision: %s, " + "aContentLocation: %s", + *outDecision > 0 ? "load" : "deny", + aContentLocation->GetSpecOrDefault().get())); + } + return NS_OK; +} + +bool +nsCSPContext::permitsInternal(CSPDirective aDir, + nsIURI* aContentLocation, + nsIURI* aOriginalURI, + const nsAString& aNonce, + bool aWasRedirected, + bool aIsPreload, + bool aSpecific, + bool aSendViolationReports, + bool aSendContentLocationInViolationReports, + bool aParserCreated) +{ + bool permits = true; + + nsAutoString violatedDirective; + for (uint32_t p = 0; p < mPolicies.Length(); p++) { + + // According to the W3C CSP spec, frame-ancestors checks are ignored for + // report-only policies (when "monitoring"). + if (aDir == nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE && + mPolicies[p]->getReportOnlyFlag()) { + continue; + } + + if (!mPolicies[p]->permits(aDir, + aContentLocation, + aNonce, + aWasRedirected, + aSpecific, + aParserCreated, + violatedDirective)) { + // If the policy is violated and not report-only, reject the load and + // report to the console + if (!mPolicies[p]->getReportOnlyFlag()) { + CSPCONTEXTLOG(("nsCSPContext::permitsInternal, false")); + permits = false; + } + + // Do not send a report or notify observers if this is a preload - the + // decision may be wrong due to the inability to get the nonce, and will + // incorrectly fail the unit tests. + if (!aIsPreload && aSendViolationReports) { + this->AsyncReportViolation((aSendContentLocationInViolationReports ? + aContentLocation : nullptr), + aOriginalURI, /* in case of redirect originalURI is not null */ + violatedDirective, + p, /* policy index */ + EmptyString(), /* no observer subject */ + EmptyString(), /* no source file */ + EmptyString(), /* no script sample */ + 0); /* no line number */ + } + } + } + + return permits; +} + + + +/* ===== nsISupports implementation ========== */ + +NS_IMPL_CLASSINFO(nsCSPContext, + nullptr, + nsIClassInfo::MAIN_THREAD_ONLY, + NS_CSPCONTEXT_CID) + +NS_IMPL_ISUPPORTS_CI(nsCSPContext, + nsIContentSecurityPolicy, + nsISerializable) + +nsCSPContext::nsCSPContext() + : mInnerWindowID(0) + , mLoadingContext(nullptr) + , mLoadingPrincipal(nullptr) + , mQueueUpMessages(true) +{ + CSPCONTEXTLOG(("nsCSPContext::nsCSPContext")); +} + +nsCSPContext::~nsCSPContext() +{ + CSPCONTEXTLOG(("nsCSPContext::~nsCSPContext")); + for (uint32_t i = 0; i < mPolicies.Length(); i++) { + delete mPolicies[i]; + } + mShouldLoadCache.Clear(); +} + +NS_IMETHODIMP +nsCSPContext::GetPolicyString(uint32_t aIndex, nsAString& outStr) +{ + if (aIndex >= mPolicies.Length()) { + return NS_ERROR_ILLEGAL_VALUE; + } + mPolicies[aIndex]->toString(outStr); + return NS_OK; +} + +const nsCSPPolicy* +nsCSPContext::GetPolicy(uint32_t aIndex) +{ + if (aIndex >= mPolicies.Length()) { + return nullptr; + } + return mPolicies[aIndex]; +} + +NS_IMETHODIMP +nsCSPContext::GetPolicyCount(uint32_t *outPolicyCount) +{ + *outPolicyCount = mPolicies.Length(); + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::GetUpgradeInsecureRequests(bool *outUpgradeRequest) +{ + *outUpgradeRequest = false; + for (uint32_t i = 0; i < mPolicies.Length(); i++) { + if (mPolicies[i]->hasDirective(nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE)) { + *outUpgradeRequest = true; + return NS_OK; + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::GetBlockAllMixedContent(bool *outBlockAllMixedContent) +{ + *outBlockAllMixedContent = false; + for (uint32_t i = 0; i < mPolicies.Length(); i++) { + if (!mPolicies[i]->getReportOnlyFlag() && + mPolicies[i]->hasDirective(nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT)) { + *outBlockAllMixedContent = true; + return NS_OK; + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::GetReferrerPolicy(uint32_t* outPolicy, bool* outIsSet) +{ + *outIsSet = false; + *outPolicy = mozilla::net::RP_Default; + nsAutoString refpol; + mozilla::net::ReferrerPolicy previousPolicy = mozilla::net::RP_Default; + for (uint32_t i = 0; i < mPolicies.Length(); i++) { + mPolicies[i]->getReferrerPolicy(refpol); + // only set the referrer policy if not delievered through a CSPRO and + // note that and an empty string in refpol means it wasn't set + // (that's the default in nsCSPPolicy). + if (!mPolicies[i]->getReportOnlyFlag() && !refpol.IsEmpty()) { + // Referrer Directive in CSP is no more used and going to be replaced by + // Referrer-Policy HTTP header. But we still keep using referrer directive, + // and would remove it later. + // Referrer Directive specs is not fully compliant with new referrer policy + // specs. What we are using here: + // - If the value of the referrer directive is invalid, the user agent + // should set the referrer policy to no-referrer. + // - If there are two policies that specify a referrer policy, then they + // must agree or the employed policy is no-referrer. + if (!mozilla::net::IsValidReferrerPolicy(refpol)) { + *outPolicy = mozilla::net::RP_No_Referrer; + *outIsSet = true; + return NS_OK; + } + + uint32_t currentPolicy = mozilla::net::ReferrerPolicyFromString(refpol); + if (*outIsSet && previousPolicy != currentPolicy) { + *outPolicy = mozilla::net::RP_No_Referrer; + return NS_OK; + } + + *outPolicy = currentPolicy; + *outIsSet = true; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::AppendPolicy(const nsAString& aPolicyString, + bool aReportOnly, + bool aDeliveredViaMetaTag) +{ + CSPCONTEXTLOG(("nsCSPContext::AppendPolicy: %s", + NS_ConvertUTF16toUTF8(aPolicyString).get())); + + // Use the mSelfURI from setRequestContext, see bug 991474 + NS_ASSERTION(mSelfURI, "mSelfURI required for AppendPolicy, but not set"); + nsCSPPolicy* policy = nsCSPParser::parseContentSecurityPolicy(aPolicyString, mSelfURI, + aReportOnly, this, + aDeliveredViaMetaTag); + if (policy) { + mPolicies.AppendElement(policy); + // reset cache since effective policy changes + mShouldLoadCache.Clear(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::GetAllowsEval(bool* outShouldReportViolation, + bool* outAllowsEval) +{ + *outShouldReportViolation = false; + *outAllowsEval = true; + + for (uint32_t i = 0; i < mPolicies.Length(); i++) { + if (!mPolicies[i]->allows(nsIContentPolicy::TYPE_SCRIPT, + CSP_UNSAFE_EVAL, + EmptyString(), + false)) { + // policy is violated: must report the violation and allow the inline + // script if the policy is report-only. + *outShouldReportViolation = true; + if (!mPolicies[i]->getReportOnlyFlag()) { + *outAllowsEval = false; + } + } + } + return NS_OK; +} + +// Helper function to report inline violations +void +nsCSPContext::reportInlineViolation(nsContentPolicyType aContentType, + const nsAString& aNonce, + const nsAString& aContent, + const nsAString& aViolatedDirective, + uint32_t aViolatedPolicyIndex, // TODO, use report only flag for that + uint32_t aLineNumber) +{ + nsString observerSubject; + // if the nonce is non empty, then we report the nonce error, otherwise + // let's report the hash error; no need to report the unsafe-inline error + // anymore. + if (!aNonce.IsEmpty()) { + observerSubject = (aContentType == nsIContentPolicy::TYPE_SCRIPT) + ? NS_LITERAL_STRING(SCRIPT_NONCE_VIOLATION_OBSERVER_TOPIC) + : NS_LITERAL_STRING(STYLE_NONCE_VIOLATION_OBSERVER_TOPIC); + } + else { + observerSubject = (aContentType == nsIContentPolicy::TYPE_SCRIPT) + ? NS_LITERAL_STRING(SCRIPT_HASH_VIOLATION_OBSERVER_TOPIC) + : NS_LITERAL_STRING(STYLE_HASH_VIOLATION_OBSERVER_TOPIC); + } + + nsCOMPtr<nsISupportsCString> selfICString(do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID)); + if (selfICString) { + selfICString->SetData(nsDependentCString("self")); + } + nsCOMPtr<nsISupports> selfISupports(do_QueryInterface(selfICString)); + + // use selfURI as the sourceFile + nsAutoCString sourceFile; + if (mSelfURI) { + mSelfURI->GetSpec(sourceFile); + } + + nsAutoString codeSample(aContent); + // cap the length of the script sample at 40 chars + if (codeSample.Length() > 40) { + codeSample.Truncate(40); + codeSample.AppendLiteral("..."); + } + AsyncReportViolation(selfISupports, // aBlockedContentSource + mSelfURI, // aOriginalURI + aViolatedDirective, // aViolatedDirective + aViolatedPolicyIndex, // aViolatedPolicyIndex + observerSubject, // aObserverSubject + NS_ConvertUTF8toUTF16(sourceFile), // aSourceFile + codeSample, // aScriptSample + aLineNumber); // aLineNum +} + +NS_IMETHODIMP +nsCSPContext::GetAllowsInline(nsContentPolicyType aContentType, + const nsAString& aNonce, + bool aParserCreated, + const nsAString& aContent, + uint32_t aLineNumber, + bool* outAllowsInline) +{ + *outAllowsInline = true; + + MOZ_ASSERT(aContentType == nsContentUtils::InternalContentPolicyTypeToExternal(aContentType), + "We should only see external content policy types here."); + + if (aContentType != nsIContentPolicy::TYPE_SCRIPT && + aContentType != nsIContentPolicy::TYPE_STYLESHEET) { + MOZ_ASSERT(false, "can only allow inline for script or style"); + return NS_OK; + } + + // always iterate all policies, otherwise we might not send out all reports + for (uint32_t i = 0; i < mPolicies.Length(); i++) { + bool allowed = + mPolicies[i]->allows(aContentType, CSP_UNSAFE_INLINE, EmptyString(), aParserCreated) || + mPolicies[i]->allows(aContentType, CSP_NONCE, aNonce, aParserCreated) || + mPolicies[i]->allows(aContentType, CSP_HASH, aContent, aParserCreated); + + if (!allowed) { + // policy is violoated: deny the load unless policy is report only and + // report the violation. + if (!mPolicies[i]->getReportOnlyFlag()) { + *outAllowsInline = false; + } + nsAutoString violatedDirective; + mPolicies[i]->getDirectiveStringForContentType(aContentType, violatedDirective); + reportInlineViolation(aContentType, + aNonce, + aContent, + violatedDirective, + i, + aLineNumber); + } + } + return NS_OK; +} + + +/** + * Reduces some code repetition for the various logging situations in + * LogViolationDetails. + * + * Call-sites for the eval/inline checks recieve two return values: allows + * and violates. Based on those, they must choose whether to call + * LogViolationDetails or not. Policies that are report-only allow the + * loads/compilations but violations should still be reported. Not all + * policies in this nsIContentSecurityPolicy instance will be violated, + * which is why we must check allows() again here. + * + * Note: This macro uses some parameters from its caller's context: + * p, mPolicies, this, aSourceFile, aScriptSample, aLineNum, selfISupports + * + * @param violationType: the VIOLATION_TYPE_* constant (partial symbol) + * such as INLINE_SCRIPT + * @param contentPolicyType: a constant from nsIContentPolicy such as TYPE_STYLESHEET + * @param nonceOrHash: for NONCE and HASH violations, it's the nonce or content + * string. For other violations, it is an empty string. + * @param keyword: the keyword corresponding to violation (UNSAFE_INLINE for most) + * @param observerTopic: the observer topic string to send with the CSP + * observer notifications. + * + * Please note that inline violations for scripts are reported within + * GetAllowsInline() and do not call this macro, hence we can pass 'false' + * as the argument _aParserCreated_ to allows(). + */ +#define CASE_CHECK_AND_REPORT(violationType, contentPolicyType, nonceOrHash, \ + keyword, observerTopic) \ + case nsIContentSecurityPolicy::VIOLATION_TYPE_ ## violationType : \ + PR_BEGIN_MACRO \ + if (!mPolicies[p]->allows(nsIContentPolicy::TYPE_ ## contentPolicyType, \ + keyword, nonceOrHash, false)) \ + { \ + nsAutoString violatedDirective; \ + mPolicies[p]->getDirectiveStringForContentType( \ + nsIContentPolicy::TYPE_ ## contentPolicyType, \ + violatedDirective); \ + this->AsyncReportViolation(selfISupports, nullptr, violatedDirective, p, \ + NS_LITERAL_STRING(observerTopic), \ + aSourceFile, aScriptSample, aLineNum); \ + } \ + PR_END_MACRO; \ + break + +/** + * For each policy, log any violation on the Error Console and send a report + * if a report-uri is present in the policy + * + * @param aViolationType + * one of the VIOLATION_TYPE_* constants, e.g. inline-script or eval + * @param aSourceFile + * name of the source file containing the violation (if available) + * @param aContentSample + * sample of the violating content (to aid debugging) + * @param aLineNum + * source line number of the violation (if available) + * @param aNonce + * (optional) If this is a nonce violation, include the nonce so we can + * recheck to determine which policies were violated and send the + * appropriate reports. + * @param aContent + * (optional) If this is a hash violation, include contents of the inline + * resource in the question so we can recheck the hash in order to + * determine which policies were violated and send the appropriate + * reports. + */ +NS_IMETHODIMP +nsCSPContext::LogViolationDetails(uint16_t aViolationType, + const nsAString& aSourceFile, + const nsAString& aScriptSample, + int32_t aLineNum, + const nsAString& aNonce, + const nsAString& aContent) +{ + for (uint32_t p = 0; p < mPolicies.Length(); p++) { + NS_ASSERTION(mPolicies[p], "null pointer in nsTArray<nsCSPPolicy>"); + + nsCOMPtr<nsISupportsCString> selfICString(do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID)); + if (selfICString) { + selfICString->SetData(nsDependentCString("self")); + } + nsCOMPtr<nsISupports> selfISupports(do_QueryInterface(selfICString)); + + switch (aViolationType) { + CASE_CHECK_AND_REPORT(EVAL, SCRIPT, NS_LITERAL_STRING(""), + CSP_UNSAFE_EVAL, EVAL_VIOLATION_OBSERVER_TOPIC); + CASE_CHECK_AND_REPORT(INLINE_STYLE, STYLESHEET, NS_LITERAL_STRING(""), + CSP_UNSAFE_INLINE, INLINE_STYLE_VIOLATION_OBSERVER_TOPIC); + CASE_CHECK_AND_REPORT(INLINE_SCRIPT, SCRIPT, NS_LITERAL_STRING(""), + CSP_UNSAFE_INLINE, INLINE_SCRIPT_VIOLATION_OBSERVER_TOPIC); + CASE_CHECK_AND_REPORT(NONCE_SCRIPT, SCRIPT, aNonce, + CSP_UNSAFE_INLINE, SCRIPT_NONCE_VIOLATION_OBSERVER_TOPIC); + CASE_CHECK_AND_REPORT(NONCE_STYLE, STYLESHEET, aNonce, + CSP_UNSAFE_INLINE, STYLE_NONCE_VIOLATION_OBSERVER_TOPIC); + CASE_CHECK_AND_REPORT(HASH_SCRIPT, SCRIPT, aContent, + CSP_UNSAFE_INLINE, SCRIPT_HASH_VIOLATION_OBSERVER_TOPIC); + CASE_CHECK_AND_REPORT(HASH_STYLE, STYLESHEET, aContent, + CSP_UNSAFE_INLINE, STYLE_HASH_VIOLATION_OBSERVER_TOPIC); + CASE_CHECK_AND_REPORT(REQUIRE_SRI_FOR_STYLE, STYLESHEET, NS_LITERAL_STRING(""), + CSP_REQUIRE_SRI_FOR, REQUIRE_SRI_STYLE_VIOLATION_OBSERVER_TOPIC); + CASE_CHECK_AND_REPORT(REQUIRE_SRI_FOR_SCRIPT, SCRIPT, NS_LITERAL_STRING(""), + CSP_REQUIRE_SRI_FOR, REQUIRE_SRI_SCRIPT_VIOLATION_OBSERVER_TOPIC); + + + default: + NS_ASSERTION(false, "LogViolationDetails with invalid type"); + break; + } + } + return NS_OK; +} + +#undef CASE_CHECK_AND_REPORT + +NS_IMETHODIMP +nsCSPContext::SetRequestContext(nsIDOMDocument* aDOMDocument, + nsIPrincipal* aPrincipal) +{ + NS_PRECONDITION(aDOMDocument || aPrincipal, + "Can't set context without doc or principal"); + NS_ENSURE_ARG(aDOMDocument || aPrincipal); + + if (aDOMDocument) { + nsCOMPtr<nsIDocument> doc = do_QueryInterface(aDOMDocument); + mLoadingContext = do_GetWeakReference(doc); + mSelfURI = doc->GetDocumentURI(); + mLoadingPrincipal = doc->NodePrincipal(); + doc->GetReferrer(mReferrer); + mInnerWindowID = doc->InnerWindowID(); + // the innerWindowID is not available for CSPs delivered through the + // header at the time setReqeustContext is called - let's queue up + // console messages until it becomes available, see flushConsoleMessages + mQueueUpMessages = !mInnerWindowID; + mCallingChannelLoadGroup = doc->GetDocumentLoadGroup(); + + // set the flag on the document for CSP telemetry + doc->SetHasCSP(true); + } + else { + CSPCONTEXTLOG(("No Document in SetRequestContext; can not query loadgroup; sending reports may fail.")); + mLoadingPrincipal = aPrincipal; + mLoadingPrincipal->GetURI(getter_AddRefs(mSelfURI)); + // if no document is available, then it also does not make sense to queue console messages + // sending messages to the browser conolse instead of the web console in that case. + mQueueUpMessages = false; + } + + NS_ASSERTION(mSelfURI, "mSelfURI not available, can not translate 'self' into actual URI"); + return NS_OK; +} + +struct ConsoleMsgQueueElem { + nsXPIDLString mMsg; + nsString mSourceName; + nsString mSourceLine; + uint32_t mLineNumber; + uint32_t mColumnNumber; + uint32_t mSeverityFlag; +}; + +void +nsCSPContext::flushConsoleMessages() +{ + // should flush messages even if doc is not available + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mLoadingContext); + if (doc) { + mInnerWindowID = doc->InnerWindowID(); + } + mQueueUpMessages = false; + + for (uint32_t i = 0; i < mConsoleMsgQueue.Length(); i++) { + ConsoleMsgQueueElem &elem = mConsoleMsgQueue[i]; + CSP_LogMessage(elem.mMsg, elem.mSourceName, elem.mSourceLine, + elem.mLineNumber, elem.mColumnNumber, + elem.mSeverityFlag, "CSP", mInnerWindowID); + } + mConsoleMsgQueue.Clear(); +} + +void +nsCSPContext::logToConsole(const char16_t* aName, + const char16_t** aParams, + uint32_t aParamsLength, + const nsAString& aSourceName, + const nsAString& aSourceLine, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag) +{ + // let's check if we have to queue up console messages + if (mQueueUpMessages) { + nsXPIDLString msg; + CSP_GetLocalizedStr(aName, aParams, aParamsLength, getter_Copies(msg)); + ConsoleMsgQueueElem &elem = *mConsoleMsgQueue.AppendElement(); + elem.mMsg = msg; + elem.mSourceName = PromiseFlatString(aSourceName); + elem.mSourceLine = PromiseFlatString(aSourceLine); + elem.mLineNumber = aLineNumber; + elem.mColumnNumber = aColumnNumber; + elem.mSeverityFlag = aSeverityFlag; + return; + } + CSP_LogLocalizedStr(aName, aParams, aParamsLength, aSourceName, + aSourceLine, aLineNumber, aColumnNumber, + aSeverityFlag, "CSP", mInnerWindowID); +} + +/** + * Strip URI for reporting according to: + * http://www.w3.org/TR/CSP/#violation-reports + * + * @param aURI + * The uri to be stripped for reporting + * @param aSelfURI + * The uri of the protected resource + * which is needed to enforce the SOP. + * @return ASCII serialization of the uri to be reported. + */ +void +StripURIForReporting(nsIURI* aURI, + nsIURI* aSelfURI, + nsACString& outStrippedURI) +{ + // 1) If the origin of uri is a globally unique identifier (for example, + // aURI has a scheme of data, blob, or filesystem), then return the + // ASCII serialization of uri’s scheme. + bool isHttpOrFtp = + (NS_SUCCEEDED(aURI->SchemeIs("http", &isHttpOrFtp)) && isHttpOrFtp) || + (NS_SUCCEEDED(aURI->SchemeIs("https", &isHttpOrFtp)) && isHttpOrFtp) || + (NS_SUCCEEDED(aURI->SchemeIs("ftp", &isHttpOrFtp)) && isHttpOrFtp); + + if (!isHttpOrFtp) { + // not strictly spec compliant, but what we really care about is + // http/https and also ftp. If it's not http/https or ftp, then treat aURI + // as if it's a globally unique identifier and just return the scheme. + aURI->GetScheme(outStrippedURI); + return; + } + + // 2) If the origin of uri is not the same as the origin of the protected + // resource, then return the ASCII serialization of uri’s origin. + if (!NS_SecurityCompareURIs(aSelfURI, aURI, false)) { + // cross origin redirects also fall into this category, see: + // http://www.w3.org/TR/CSP/#violation-reports + aURI->GetPrePath(outStrippedURI); + return; + } + + // 3) Return uri, with any fragment component removed. + aURI->GetSpecIgnoringRef(outStrippedURI); +} + +/** + * Sends CSP violation reports to all sources listed under report-uri. + * + * @param aBlockedContentSource + * Either a CSP Source (like 'self', as string) or nsIURI: the source + * of the violation. + * @param aOriginalUri + * The original URI if the blocked content is a redirect, else null + * @param aViolatedDirective + * the directive that was violated (string). + * @param aSourceFile + * name of the file containing the inline script violation + * @param aScriptSample + * a sample of the violating inline script + * @param aLineNum + * source line number of the violation (if available) + */ +nsresult +nsCSPContext::SendReports(nsISupports* aBlockedContentSource, + nsIURI* aOriginalURI, + nsAString& aViolatedDirective, + uint32_t aViolatedPolicyIndex, + nsAString& aSourceFile, + nsAString& aScriptSample, + uint32_t aLineNum) +{ + NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1); + +#ifdef MOZ_B2G + // load group information (on process-split necko implementations like b2g). + // (fix this in bug 1011086) + if (!mCallingChannelLoadGroup) { + NS_WARNING("Load group required but not present for report sending; cannot send CSP violation reports"); + return NS_ERROR_FAILURE; + } +#endif + + dom::CSPReport report; + nsresult rv; + + // blocked-uri + if (aBlockedContentSource) { + nsAutoCString reportBlockedURI; + nsCOMPtr<nsIURI> uri = do_QueryInterface(aBlockedContentSource); + // could be a string or URI + if (uri) { + StripURIForReporting(uri, mSelfURI, reportBlockedURI); + } else { + nsCOMPtr<nsISupportsCString> cstr = do_QueryInterface(aBlockedContentSource); + if (cstr) { + cstr->GetData(reportBlockedURI); + } + } + if (reportBlockedURI.IsEmpty()) { + // this can happen for frame-ancestors violation where the violating + // ancestor is cross-origin. + NS_WARNING("No blocked URI (null aBlockedContentSource) for CSP violation report."); + } + report.mCsp_report.mBlocked_uri = NS_ConvertUTF8toUTF16(reportBlockedURI); + } + + // document-uri + nsAutoCString reportDocumentURI; + StripURIForReporting(mSelfURI, mSelfURI, reportDocumentURI); + report.mCsp_report.mDocument_uri = NS_ConvertUTF8toUTF16(reportDocumentURI); + + // original-policy + nsAutoString originalPolicy; + rv = this->GetPolicyString(aViolatedPolicyIndex, originalPolicy); + NS_ENSURE_SUCCESS(rv, rv); + report.mCsp_report.mOriginal_policy = originalPolicy; + + // referrer + if (!mReferrer.IsEmpty()) { + report.mCsp_report.mReferrer = mReferrer; + } + + // violated-directive + report.mCsp_report.mViolated_directive = aViolatedDirective; + + // source-file + if (!aSourceFile.IsEmpty()) { + // if aSourceFile is a URI, we have to make sure to strip fragments + nsCOMPtr<nsIURI> sourceURI; + NS_NewURI(getter_AddRefs(sourceURI), aSourceFile); + if (sourceURI) { + nsAutoCString spec; + sourceURI->GetSpecIgnoringRef(spec); + aSourceFile = NS_ConvertUTF8toUTF16(spec); + } + report.mCsp_report.mSource_file.Construct(); + report.mCsp_report.mSource_file.Value() = aSourceFile; + } + + // script-sample + if (!aScriptSample.IsEmpty()) { + report.mCsp_report.mScript_sample.Construct(); + report.mCsp_report.mScript_sample.Value() = aScriptSample; + } + + // line-number + if (aLineNum != 0) { + report.mCsp_report.mLine_number.Construct(); + report.mCsp_report.mLine_number.Value() = aLineNum; + } + + nsString csp_report; + if (!report.ToJSON(csp_report)) { + return NS_ERROR_FAILURE; + } + + // ---------- Assembled, now send it to all the report URIs ----------- // + + nsTArray<nsString> reportURIs; + mPolicies[aViolatedPolicyIndex]->getReportURIs(reportURIs); + + + nsCOMPtr<nsIDocument> doc = do_QueryReferent(mLoadingContext); + nsCOMPtr<nsIURI> reportURI; + nsCOMPtr<nsIChannel> reportChannel; + + for (uint32_t r = 0; r < reportURIs.Length(); r++) { + nsAutoCString reportURICstring = NS_ConvertUTF16toUTF8(reportURIs[r]); + // try to create a new uri from every report-uri string + rv = NS_NewURI(getter_AddRefs(reportURI), reportURIs[r]); + if (NS_FAILED(rv)) { + const char16_t* params[] = { reportURIs[r].get() }; + CSPCONTEXTLOG(("Could not create nsIURI for report URI %s", + reportURICstring.get())); + logToConsole(u"triedToSendReport", params, ArrayLength(params), + aSourceFile, aScriptSample, aLineNum, 0, nsIScriptError::errorFlag); + continue; // don't return yet, there may be more URIs + } + + // try to create a new channel for every report-uri + nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL | nsIChannel::LOAD_CLASSIFY_URI; + if (doc) { + rv = NS_NewChannel(getter_AddRefs(reportChannel), + reportURI, + doc, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_CSP_REPORT, + nullptr, // aLoadGroup + nullptr, // aCallbacks + loadFlags); + } + else { + rv = NS_NewChannel(getter_AddRefs(reportChannel), + reportURI, + mLoadingPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_CSP_REPORT, + nullptr, // aLoadGroup + nullptr, // aCallbacks + loadFlags); + } + + if (NS_FAILED(rv)) { + CSPCONTEXTLOG(("Could not create new channel for report URI %s", + reportURICstring.get())); + continue; // don't return yet, there may be more URIs + } + + // log a warning to console if scheme is not http or https + bool isHttpScheme = + (NS_SUCCEEDED(reportURI->SchemeIs("http", &isHttpScheme)) && isHttpScheme) || + (NS_SUCCEEDED(reportURI->SchemeIs("https", &isHttpScheme)) && isHttpScheme); + + if (!isHttpScheme) { + const char16_t* params[] = { reportURIs[r].get() }; + logToConsole(u"reportURInotHttpsOrHttp2", params, ArrayLength(params), + aSourceFile, aScriptSample, aLineNum, 0, nsIScriptError::errorFlag); + continue; + } + + // make sure this is an anonymous request (no cookies) so in case the + // policy URI is injected, it can't be abused for CSRF. + nsLoadFlags flags; + rv = reportChannel->GetLoadFlags(&flags); + NS_ENSURE_SUCCESS(rv, rv); + flags |= nsIRequest::LOAD_ANONYMOUS; + rv = reportChannel->SetLoadFlags(flags); + NS_ENSURE_SUCCESS(rv, rv); + + // we need to set an nsIChannelEventSink on the channel object + // so we can tell it to not follow redirects when posting the reports + RefPtr<CSPReportRedirectSink> reportSink = new CSPReportRedirectSink(); + if (doc && doc->GetDocShell()) { + nsCOMPtr<nsINetworkInterceptController> interceptController = + do_QueryInterface(doc->GetDocShell()); + reportSink->SetInterceptController(interceptController); + } + reportChannel->SetNotificationCallbacks(reportSink); + + // apply the loadgroup from the channel taken by setRequestContext. If + // there's no loadgroup, AsyncOpen will fail on process-split necko (since + // the channel cannot query the iTabChild). + rv = reportChannel->SetLoadGroup(mCallingChannelLoadGroup); + NS_ENSURE_SUCCESS(rv, rv); + + // wire in the string input stream to send the report + nsCOMPtr<nsIStringInputStream> sis(do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID)); + NS_ASSERTION(sis, "nsIStringInputStream is needed but not available to send CSP violation reports"); + nsAutoCString utf8CSPReport = NS_ConvertUTF16toUTF8(csp_report); + rv = sis->SetData(utf8CSPReport.get(), utf8CSPReport.Length()); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIUploadChannel> uploadChannel(do_QueryInterface(reportChannel)); + if (!uploadChannel) { + // It's possible the URI provided can't be uploaded to, in which case + // we skip this one. We'll already have warned about a non-HTTP URI earlier. + continue; + } + + rv = uploadChannel->SetUploadStream(sis, NS_LITERAL_CSTRING("application/csp-report"), -1); + NS_ENSURE_SUCCESS(rv, rv); + + // if this is an HTTP channel, set the request method to post + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(reportChannel)); + if (httpChannel) { + httpChannel->SetRequestMethod(NS_LITERAL_CSTRING("POST")); + } + + RefPtr<CSPViolationReportListener> listener = new CSPViolationReportListener(); + rv = reportChannel->AsyncOpen2(listener); + + // AsyncOpen should not fail, but could if there's no load group (like if + // SetRequestContext is not given a channel). This should fail quietly and + // not return an error since it's really ok if reports don't go out, but + // it's good to log the error locally. + + if (NS_FAILED(rv)) { + const char16_t* params[] = { reportURIs[r].get() }; + CSPCONTEXTLOG(("AsyncOpen failed for report URI %s", params[0])); + logToConsole(u"triedToSendReport", params, ArrayLength(params), + aSourceFile, aScriptSample, aLineNum, 0, nsIScriptError::errorFlag); + } else { + CSPCONTEXTLOG(("Sent violation report to URI %s", reportURICstring.get())); + } + } + return NS_OK; +} + +/** + * Dispatched from the main thread to send reports for one CSP violation. + */ +class CSPReportSenderRunnable final : public Runnable +{ + public: + CSPReportSenderRunnable(nsISupports* aBlockedContentSource, + nsIURI* aOriginalURI, + uint32_t aViolatedPolicyIndex, + bool aReportOnlyFlag, + const nsAString& aViolatedDirective, + const nsAString& aObserverSubject, + const nsAString& aSourceFile, + const nsAString& aScriptSample, + uint32_t aLineNum, + nsCSPContext* aCSPContext) + : mBlockedContentSource(aBlockedContentSource) + , mOriginalURI(aOriginalURI) + , mViolatedPolicyIndex(aViolatedPolicyIndex) + , mReportOnlyFlag(aReportOnlyFlag) + , mViolatedDirective(aViolatedDirective) + , mSourceFile(aSourceFile) + , mScriptSample(aScriptSample) + , mLineNum(aLineNum) + , mCSPContext(aCSPContext) + { + NS_ASSERTION(!aViolatedDirective.IsEmpty(), "Can not send reports without a violated directive"); + // the observer subject is an nsISupports: either an nsISupportsCString + // from the arg passed in directly, or if that's empty, it's the blocked + // source. + if (aObserverSubject.IsEmpty()) { + mObserverSubject = aBlockedContentSource; + } else { + nsCOMPtr<nsISupportsCString> supportscstr = + do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID); + NS_ASSERTION(supportscstr, "Couldn't allocate nsISupportsCString"); + supportscstr->SetData(NS_ConvertUTF16toUTF8(aObserverSubject)); + mObserverSubject = do_QueryInterface(supportscstr); + } + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread()); + + // 1) notify observers + nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService(); + NS_ASSERTION(observerService, "needs observer service"); + nsresult rv = observerService->NotifyObservers(mObserverSubject, + CSP_VIOLATION_TOPIC, + mViolatedDirective.get()); + NS_ENSURE_SUCCESS(rv, rv); + + // 2) send reports for the policy that was violated + mCSPContext->SendReports(mBlockedContentSource, mOriginalURI, + mViolatedDirective, mViolatedPolicyIndex, + mSourceFile, mScriptSample, mLineNum); + + // 3) log to console (one per policy violation) + // mBlockedContentSource could be a URI or a string. + nsCOMPtr<nsIURI> blockedURI = do_QueryInterface(mBlockedContentSource); + // if mBlockedContentSource is not a URI, it could be a string + nsCOMPtr<nsISupportsCString> blockedString = do_QueryInterface(mBlockedContentSource); + + nsCString blockedDataStr; + + if (blockedURI) { + blockedURI->GetSpec(blockedDataStr); + bool isData = false; + rv = blockedURI->SchemeIs("data", &isData); + if (NS_SUCCEEDED(rv) && isData) { + blockedDataStr.Truncate(40); + blockedDataStr.AppendASCII("..."); + } + } else if (blockedString) { + blockedString->GetData(blockedDataStr); + } + + if (blockedDataStr.Length() > 0) { + nsString blockedDataChar16 = NS_ConvertUTF8toUTF16(blockedDataStr); + const char16_t* params[] = { mViolatedDirective.get(), + blockedDataChar16.get() }; + mCSPContext->logToConsole(mReportOnlyFlag ? u"CSPROViolationWithURI" : + u"CSPViolationWithURI", + params, ArrayLength(params), mSourceFile, mScriptSample, + mLineNum, 0, nsIScriptError::errorFlag); + } + return NS_OK; + } + + private: + nsCOMPtr<nsISupports> mBlockedContentSource; + nsCOMPtr<nsIURI> mOriginalURI; + uint32_t mViolatedPolicyIndex; + bool mReportOnlyFlag; + nsString mViolatedDirective; + nsCOMPtr<nsISupports> mObserverSubject; + nsString mSourceFile; + nsString mScriptSample; + uint32_t mLineNum; + RefPtr<nsCSPContext> mCSPContext; +}; + +/** + * Asynchronously notifies any nsIObservers listening to the CSP violation + * topic that a violation occurred. Also triggers report sending and console + * logging. All asynchronous on the main thread. + * + * @param aBlockedContentSource + * Either a CSP Source (like 'self', as string) or nsIURI: the source + * of the violation. + * @param aOriginalUri + * The original URI if the blocked content is a redirect, else null + * @param aViolatedDirective + * the directive that was violated (string). + * @param aViolatedPolicyIndex + * the index of the policy that was violated (so we know where to send + * the reports). + * @param aObserverSubject + * optional, subject sent to the nsIObservers listening to the CSP + * violation topic. + * @param aSourceFile + * name of the file containing the inline script violation + * @param aScriptSample + * a sample of the violating inline script + * @param aLineNum + * source line number of the violation (if available) + */ +nsresult +nsCSPContext::AsyncReportViolation(nsISupports* aBlockedContentSource, + nsIURI* aOriginalURI, + const nsAString& aViolatedDirective, + uint32_t aViolatedPolicyIndex, + const nsAString& aObserverSubject, + const nsAString& aSourceFile, + const nsAString& aScriptSample, + uint32_t aLineNum) +{ + NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1); + + NS_DispatchToMainThread(new CSPReportSenderRunnable(aBlockedContentSource, + aOriginalURI, + aViolatedPolicyIndex, + mPolicies[aViolatedPolicyIndex]->getReportOnlyFlag(), + aViolatedDirective, + aObserverSubject, + aSourceFile, + aScriptSample, + aLineNum, + this)); + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::RequireSRIForType(nsContentPolicyType aContentType, bool* outRequiresSRIForType) +{ + *outRequiresSRIForType = false; + for (uint32_t i = 0; i < mPolicies.Length(); i++) { + if (mPolicies[i]->hasDirective(REQUIRE_SRI_FOR)) { + if (mPolicies[i]->requireSRIForType(aContentType)) { + *outRequiresSRIForType = true; + return NS_OK; + } + } + } + return NS_OK; +} + +/** + * Based on the given docshell, determines if this CSP context allows the + * ancestry. + * + * In order to determine the URI of the parent document (one causing the load + * of this protected document), this function obtains the docShellTreeItem, + * then walks up the hierarchy until it finds a privileged (chrome) tree item. + * Getting the a tree item's URI looks like this in pseudocode: + * + * nsIDocShellTreeItem->GetDocument()->GetDocumentURI(); + * + * aDocShell is the docShell for the protected document. + */ +NS_IMETHODIMP +nsCSPContext::PermitsAncestry(nsIDocShell* aDocShell, bool* outPermitsAncestry) +{ + nsresult rv; + + // Can't check ancestry without a docShell. + if (aDocShell == nullptr) { + return NS_ERROR_FAILURE; + } + + *outPermitsAncestry = true; + + // extract the ancestry as an array + nsCOMArray<nsIURI> ancestorsArray; + + nsCOMPtr<nsIInterfaceRequestor> ir(do_QueryInterface(aDocShell)); + nsCOMPtr<nsIDocShellTreeItem> treeItem(do_GetInterface(ir)); + nsCOMPtr<nsIDocShellTreeItem> parentTreeItem; + nsCOMPtr<nsIURI> currentURI; + nsCOMPtr<nsIURI> uriClone; + + // iterate through each docShell parent item + while (NS_SUCCEEDED(treeItem->GetParent(getter_AddRefs(parentTreeItem))) && + parentTreeItem != nullptr) { + + nsIDocument* doc = parentTreeItem->GetDocument(); + NS_ASSERTION(doc, "Could not get nsIDocument from nsIDocShellTreeItem in nsCSPContext::PermitsAncestry"); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + + currentURI = doc->GetDocumentURI(); + + if (currentURI) { + // stop when reaching chrome + bool isChrome = false; + rv = currentURI->SchemeIs("chrome", &isChrome); + NS_ENSURE_SUCCESS(rv, rv); + if (isChrome) { break; } + + // delete the userpass from the URI. + rv = currentURI->CloneIgnoringRef(getter_AddRefs(uriClone)); + NS_ENSURE_SUCCESS(rv, rv); + + // We don't care if this succeeds, just want to delete a userpass if + // there was one. + uriClone->SetUserPass(EmptyCString()); + + if (CSPCONTEXTLOGENABLED()) { + CSPCONTEXTLOG(("nsCSPContext::PermitsAncestry, found ancestor: %s", + uriClone->GetSpecOrDefault().get())); + } + ancestorsArray.AppendElement(uriClone); + } + + // next ancestor + treeItem = parentTreeItem; + } + + nsAutoString violatedDirective; + + // Now that we've got the ancestry chain in ancestorsArray, time to check + // them against any CSP. + // NOTE: the ancestors are not allowed to be sent cross origin; this is a + // restriction not placed on subresource loads. + + for (uint32_t a = 0; a < ancestorsArray.Length(); a++) { + if (CSPCONTEXTLOGENABLED()) { + CSPCONTEXTLOG(("nsCSPContext::PermitsAncestry, checking ancestor: %s", + ancestorsArray[a]->GetSpecOrDefault().get())); + } + // omit the ancestor URI in violation reports if cross-origin as per spec + // (it is a violation of the same-origin policy). + bool okToSendAncestor = NS_SecurityCompareURIs(ancestorsArray[a], mSelfURI, true); + + + bool permits = permitsInternal(nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE, + ancestorsArray[a], + nullptr, // no redirect here. + EmptyString(), // no nonce + false, // no redirect here. + false, // not a preload. + true, // specific, do not use default-src + true, // send violation reports + okToSendAncestor, + false); // not parser created + if (!permits) { + *outPermitsAncestry = false; + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::Permits(nsIURI* aURI, + CSPDirective aDir, + bool aSpecific, + bool* outPermits) +{ + // Can't perform check without aURI + if (aURI == nullptr) { + return NS_ERROR_FAILURE; + } + + *outPermits = permitsInternal(aDir, + aURI, + nullptr, // no original (pre-redirect) URI + EmptyString(), // no nonce + false, // not redirected. + false, // not a preload. + aSpecific, + true, // send violation reports + true, // send blocked URI in violation reports + false); // not parser created + + if (CSPCONTEXTLOGENABLED()) { + CSPCONTEXTLOG(("nsCSPContext::Permits, aUri: %s, aDir: %d, isAllowed: %s", + aURI->GetSpecOrDefault().get(), aDir, + *outPermits ? "allow" : "deny")); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::ToJSON(nsAString& outCSPinJSON) +{ + outCSPinJSON.Truncate(); + dom::CSPPolicies jsonPolicies; + jsonPolicies.mCsp_policies.Construct(); + + for (uint32_t p = 0; p < mPolicies.Length(); p++) { + dom::CSP jsonCSP; + mPolicies[p]->toDomCSPStruct(jsonCSP); + jsonPolicies.mCsp_policies.Value().AppendElement(jsonCSP, fallible); + } + + // convert the gathered information to JSON + if (!jsonPolicies.ToJSON(outCSPinJSON)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::GetCSPSandboxFlags(uint32_t* aOutSandboxFlags) +{ + if (!aOutSandboxFlags) { + return NS_ERROR_FAILURE; + } + *aOutSandboxFlags = SANDBOXED_NONE; + + for (uint32_t i = 0; i < mPolicies.Length(); i++) { + uint32_t flags = mPolicies[i]->getSandboxFlags(); + + // current policy doesn't have sandbox flag, check next policy + if (!flags) { + continue; + } + + // current policy has sandbox flags, if the policy is in enforcement-mode + // (i.e. not report-only) set these flags and check for policies with more + // restrictions + if (!mPolicies[i]->getReportOnlyFlag()) { + *aOutSandboxFlags |= flags; + } else { + // sandbox directive is ignored in report-only mode, warn about it and + // continue the loop checking for an enforcement policy. + nsAutoString policy; + mPolicies[i]->toString(policy); + + CSPCONTEXTLOG(("nsCSPContext::GetCSPSandboxFlags, report only policy, ignoring sandbox in: %s", + policy.get())); + + const char16_t* params[] = { policy.get() }; + logToConsole(u"ignoringReportOnlyDirective", params, ArrayLength(params), + EmptyString(), EmptyString(), 0, 0, nsIScriptError::warningFlag); + } + } + + return NS_OK; +} + +/* ========== CSPViolationReportListener implementation ========== */ + +NS_IMPL_ISUPPORTS(CSPViolationReportListener, nsIStreamListener, nsIRequestObserver, nsISupports); + +CSPViolationReportListener::CSPViolationReportListener() +{ +} + +CSPViolationReportListener::~CSPViolationReportListener() +{ +} + +nsresult +AppendSegmentToString(nsIInputStream* aInputStream, + void* aClosure, + const char* aRawSegment, + uint32_t aToOffset, + uint32_t aCount, + uint32_t* outWrittenCount) +{ + nsCString* decodedData = static_cast<nsCString*>(aClosure); + decodedData->Append(aRawSegment, aCount); + *outWrittenCount = aCount; + return NS_OK; +} + +NS_IMETHODIMP +CSPViolationReportListener::OnDataAvailable(nsIRequest* aRequest, + nsISupports* aContext, + nsIInputStream* aInputStream, + uint64_t aOffset, + uint32_t aCount) +{ + uint32_t read; + nsCString decodedData; + return aInputStream->ReadSegments(AppendSegmentToString, + &decodedData, + aCount, + &read); +} + +NS_IMETHODIMP +CSPViolationReportListener::OnStopRequest(nsIRequest* aRequest, + nsISupports* aContext, + nsresult aStatus) +{ + return NS_OK; +} + +NS_IMETHODIMP +CSPViolationReportListener::OnStartRequest(nsIRequest* aRequest, + nsISupports* aContext) +{ + return NS_OK; +} + +/* ========== CSPReportRedirectSink implementation ========== */ + +NS_IMPL_ISUPPORTS(CSPReportRedirectSink, nsIChannelEventSink, nsIInterfaceRequestor); + +CSPReportRedirectSink::CSPReportRedirectSink() +{ +} + +CSPReportRedirectSink::~CSPReportRedirectSink() +{ +} + +NS_IMETHODIMP +CSPReportRedirectSink::AsyncOnChannelRedirect(nsIChannel* aOldChannel, + nsIChannel* aNewChannel, + uint32_t aRedirFlags, + nsIAsyncVerifyRedirectCallback* aCallback) +{ + // cancel the old channel so XHR failure callback happens + nsresult rv = aOldChannel->Cancel(NS_ERROR_ABORT); + NS_ENSURE_SUCCESS(rv, rv); + + // notify an observer that we have blocked the report POST due to a redirect, + // used in testing, do this async since we're in an async call now to begin with + nsCOMPtr<nsIURI> uri; + rv = aOldChannel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService(); + NS_ASSERTION(observerService, "Observer service required to log CSP violations"); + observerService->NotifyObservers(uri, + CSP_VIOLATION_TOPIC, + u"denied redirect while sending violation report"); + + return NS_BINDING_REDIRECTED; +} + +NS_IMETHODIMP +CSPReportRedirectSink::GetInterface(const nsIID& aIID, void** aResult) +{ + if (aIID.Equals(NS_GET_IID(nsINetworkInterceptController)) && + mInterceptController) { + nsCOMPtr<nsINetworkInterceptController> copy(mInterceptController); + *aResult = copy.forget().take(); + + return NS_OK; + } + + return QueryInterface(aIID, aResult); +} + +void +CSPReportRedirectSink::SetInterceptController(nsINetworkInterceptController* aInterceptController) +{ + mInterceptController = aInterceptController; +} + +/* ===== nsISerializable implementation ====== */ + +NS_IMETHODIMP +nsCSPContext::Read(nsIObjectInputStream* aStream) +{ + nsresult rv; + nsCOMPtr<nsISupports> supports; + + rv = NS_ReadOptionalObject(aStream, true, getter_AddRefs(supports)); + NS_ENSURE_SUCCESS(rv, rv); + + mSelfURI = do_QueryInterface(supports); + NS_ASSERTION(mSelfURI, "need a self URI to de-serialize"); + + uint32_t numPolicies; + rv = aStream->Read32(&numPolicies); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString policyString; + + while (numPolicies > 0) { + numPolicies--; + + rv = aStream->ReadString(policyString); + NS_ENSURE_SUCCESS(rv, rv); + + bool reportOnly = false; + rv = aStream->ReadBoolean(&reportOnly); + NS_ENSURE_SUCCESS(rv, rv); + + // @param deliveredViaMetaTag: + // when parsing the CSP policy string initially we already remove directives + // that should not be processed when delivered via the meta tag. Such directives + // will not be present at this point anymore. + nsCSPPolicy* policy = nsCSPParser::parseContentSecurityPolicy(policyString, + mSelfURI, + reportOnly, + this, + false); + if (policy) { + mPolicies.AppendElement(policy); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::Write(nsIObjectOutputStream* aStream) +{ + nsresult rv = NS_WriteOptionalCompoundObject(aStream, + mSelfURI, + NS_GET_IID(nsIURI), + true); + NS_ENSURE_SUCCESS(rv, rv); + + // Serialize all the policies. + aStream->Write32(mPolicies.Length()); + + nsAutoString polStr; + for (uint32_t p = 0; p < mPolicies.Length(); p++) { + polStr.Truncate(); + mPolicies[p]->toString(polStr); + aStream->WriteWStringZ(polStr.get()); + aStream->WriteBoolean(mPolicies[p]->getReportOnlyFlag()); + } + return NS_OK; +} diff --git a/dom/security/nsCSPContext.h b/dom/security/nsCSPContext.h new file mode 100644 index 000000000..729f440ce --- /dev/null +++ b/dom/security/nsCSPContext.h @@ -0,0 +1,164 @@ +/* -*- 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 nsCSPContext_h___ +#define nsCSPContext_h___ + +#include "mozilla/dom/nsCSPUtils.h" +#include "nsDataHashtable.h" +#include "nsIChannel.h" +#include "nsIChannelEventSink.h" +#include "nsIClassInfo.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIInterfaceRequestor.h" +#include "nsISerializable.h" +#include "nsIStreamListener.h" +#include "nsWeakReference.h" +#include "nsXPCOM.h" + +#define NS_CSPCONTEXT_CONTRACTID "@mozilla.org/cspcontext;1" + // 09d9ed1a-e5d4-4004-bfe0-27ceb923d9ac +#define NS_CSPCONTEXT_CID \ +{ 0x09d9ed1a, 0xe5d4, 0x4004, \ + { 0xbf, 0xe0, 0x27, 0xce, 0xb9, 0x23, 0xd9, 0xac } } + +class nsINetworkInterceptController; +struct ConsoleMsgQueueElem; + +class nsCSPContext : public nsIContentSecurityPolicy +{ + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTSECURITYPOLICY + NS_DECL_NSISERIALIZABLE + + protected: + virtual ~nsCSPContext(); + + public: + nsCSPContext(); + + /** + * SetRequestContext() needs to be called before the innerWindowID + * is initialized on the document. Use this function to call back to + * flush queued up console messages and initalize the innerWindowID. + */ + void flushConsoleMessages(); + + void logToConsole(const char16_t* aName, + const char16_t** aParams, + uint32_t aParamsLength, + const nsAString& aSourceName, + const nsAString& aSourceLine, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag); + + nsresult SendReports(nsISupports* aBlockedContentSource, + nsIURI* aOriginalURI, + nsAString& aViolatedDirective, + uint32_t aViolatedPolicyIndex, + nsAString& aSourceFile, + nsAString& aScriptSample, + uint32_t aLineNum); + + nsresult AsyncReportViolation(nsISupports* aBlockedContentSource, + nsIURI* aOriginalURI, + const nsAString& aViolatedDirective, + uint32_t aViolatedPolicyIndex, + const nsAString& aObserverSubject, + const nsAString& aSourceFile, + const nsAString& aScriptSample, + uint32_t aLineNum); + + // Hands off! Don't call this method unless you know what you + // are doing. It's only supposed to be called from within + // the principal destructor to avoid a tangling pointer. + void clearLoadingPrincipal() { + mLoadingPrincipal = nullptr; + } + + nsWeakPtr GetLoadingContext(){ + return mLoadingContext; + } + + private: + bool permitsInternal(CSPDirective aDir, + nsIURI* aContentLocation, + nsIURI* aOriginalURI, + const nsAString& aNonce, + bool aWasRedirected, + bool aIsPreload, + bool aSpecific, + bool aSendViolationReports, + bool aSendContentLocationInViolationReports, + bool aParserCreated); + + // helper to report inline script/style violations + void reportInlineViolation(nsContentPolicyType aContentType, + const nsAString& aNonce, + const nsAString& aContent, + const nsAString& aViolatedDirective, + uint32_t aViolatedPolicyIndex, + uint32_t aLineNumber); + + nsString mReferrer; + uint64_t mInnerWindowID; // used for web console logging + nsTArray<nsCSPPolicy*> mPolicies; + nsCOMPtr<nsIURI> mSelfURI; + nsDataHashtable<nsCStringHashKey, int16_t> mShouldLoadCache; + nsCOMPtr<nsILoadGroup> mCallingChannelLoadGroup; + nsWeakPtr mLoadingContext; + // The CSP hangs off the principal, so let's store a raw pointer of the principal + // to avoid memory leaks. Within the destructor of the principal we explicitly + // set mLoadingPrincipal to null. + nsIPrincipal* mLoadingPrincipal; + + // helper members used to queue up web console messages till + // the windowID becomes available. see flushConsoleMessages() + nsTArray<ConsoleMsgQueueElem> mConsoleMsgQueue; + bool mQueueUpMessages; +}; + +// Class that listens to violation report transmission and logs errors. +class CSPViolationReportListener : public nsIStreamListener +{ + public: + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_ISUPPORTS + + public: + CSPViolationReportListener(); + + protected: + virtual ~CSPViolationReportListener(); +}; + +// The POST of the violation report (if it happens) should not follow +// redirects, per the spec. hence, we implement an nsIChannelEventSink +// with an object so we can tell XHR to abort if a redirect happens. +class CSPReportRedirectSink final : public nsIChannelEventSink, + public nsIInterfaceRequestor +{ + public: + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_ISUPPORTS + + public: + CSPReportRedirectSink(); + + void SetInterceptController(nsINetworkInterceptController* aInterceptController); + + protected: + virtual ~CSPReportRedirectSink(); + + private: + nsCOMPtr<nsINetworkInterceptController> mInterceptController; +}; + +#endif /* nsCSPContext_h___ */ diff --git a/dom/security/nsCSPParser.cpp b/dom/security/nsCSPParser.cpp new file mode 100644 index 000000000..a662c9cd1 --- /dev/null +++ b/dom/security/nsCSPParser.cpp @@ -0,0 +1,1358 @@ +/* -*- 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 "mozilla/ArrayUtils.h" +#include "mozilla/Preferences.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsCSPParser.h" +#include "nsCSPUtils.h" +#include "nsIConsoleService.h" +#include "nsIContentPolicy.h" +#include "nsIScriptError.h" +#include "nsIStringBundle.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsUnicharUtils.h" +#include "mozilla/net/ReferrerPolicy.h" + +using namespace mozilla; + +static LogModule* +GetCspParserLog() +{ + static LazyLogModule gCspParserPRLog("CSPParser"); + return gCspParserPRLog; +} + +#define CSPPARSERLOG(args) MOZ_LOG(GetCspParserLog(), mozilla::LogLevel::Debug, args) +#define CSPPARSERLOGENABLED() MOZ_LOG_TEST(GetCspParserLog(), mozilla::LogLevel::Debug) + +static const char16_t COLON = ':'; +static const char16_t SEMICOLON = ';'; +static const char16_t SLASH = '/'; +static const char16_t PLUS = '+'; +static const char16_t DASH = '-'; +static const char16_t DOT = '.'; +static const char16_t UNDERLINE = '_'; +static const char16_t TILDE = '~'; +static const char16_t WILDCARD = '*'; +static const char16_t SINGLEQUOTE = '\''; +static const char16_t OPEN_CURL = '{'; +static const char16_t CLOSE_CURL = '}'; +static const char16_t NUMBER_SIGN = '#'; +static const char16_t QUESTIONMARK = '?'; +static const char16_t PERCENT_SIGN = '%'; +static const char16_t EXCLAMATION = '!'; +static const char16_t DOLLAR = '$'; +static const char16_t AMPERSAND = '&'; +static const char16_t OPENBRACE = '('; +static const char16_t CLOSINGBRACE = ')'; +static const char16_t EQUALS = '='; +static const char16_t ATSYMBOL = '@'; + +static const uint32_t kSubHostPathCharacterCutoff = 512; + +static const char *const kHashSourceValidFns [] = { "sha256", "sha384", "sha512" }; +static const uint32_t kHashSourceValidFnsLen = 3; + +static const char* const kStyle = "style"; +static const char* const kScript = "script"; + +/* ===== nsCSPTokenizer ==================== */ + +nsCSPTokenizer::nsCSPTokenizer(const char16_t* aStart, + const char16_t* aEnd) + : mCurChar(aStart) + , mEndChar(aEnd) +{ + CSPPARSERLOG(("nsCSPTokenizer::nsCSPTokenizer")); +} + +nsCSPTokenizer::~nsCSPTokenizer() +{ + CSPPARSERLOG(("nsCSPTokenizer::~nsCSPTokenizer")); +} + +void +nsCSPTokenizer::generateNextToken() +{ + skipWhiteSpaceAndSemicolon(); + while (!atEnd() && + !nsContentUtils::IsHTMLWhitespace(*mCurChar) && + *mCurChar != SEMICOLON) { + mCurToken.Append(*mCurChar++); + } + CSPPARSERLOG(("nsCSPTokenizer::generateNextToken: %s", NS_ConvertUTF16toUTF8(mCurToken).get())); +} + +void +nsCSPTokenizer::generateTokens(cspTokens& outTokens) +{ + CSPPARSERLOG(("nsCSPTokenizer::generateTokens")); + + // dirAndSrcs holds one set of [ name, src, src, src, ... ] + nsTArray <nsString> dirAndSrcs; + + while (!atEnd()) { + generateNextToken(); + dirAndSrcs.AppendElement(mCurToken); + skipWhiteSpace(); + if (atEnd() || accept(SEMICOLON)) { + outTokens.AppendElement(dirAndSrcs); + dirAndSrcs.Clear(); + } + } +} + +void +nsCSPTokenizer::tokenizeCSPPolicy(const nsAString &aPolicyString, + cspTokens& outTokens) +{ + CSPPARSERLOG(("nsCSPTokenizer::tokenizeCSPPolicy")); + + nsCSPTokenizer tokenizer(aPolicyString.BeginReading(), + aPolicyString.EndReading()); + + tokenizer.generateTokens(outTokens); +} + +/* ===== nsCSPParser ==================== */ +bool nsCSPParser::sCSPExperimentalEnabled = false; +bool nsCSPParser::sStrictDynamicEnabled = false; + +nsCSPParser::nsCSPParser(cspTokens& aTokens, + nsIURI* aSelfURI, + nsCSPContext* aCSPContext, + bool aDeliveredViaMetaTag) + : mCurChar(nullptr) + , mEndChar(nullptr) + , mHasHashOrNonce(false) + , mStrictDynamic(false) + , mUnsafeInlineKeywordSrc(nullptr) + , mChildSrc(nullptr) + , mFrameSrc(nullptr) + , mTokens(aTokens) + , mSelfURI(aSelfURI) + , mPolicy(nullptr) + , mCSPContext(aCSPContext) + , mDeliveredViaMetaTag(aDeliveredViaMetaTag) +{ + static bool initialized = false; + if (!initialized) { + initialized = true; + Preferences::AddBoolVarCache(&sCSPExperimentalEnabled, "security.csp.experimentalEnabled"); + Preferences::AddBoolVarCache(&sStrictDynamicEnabled, "security.csp.enableStrictDynamic"); + } + CSPPARSERLOG(("nsCSPParser::nsCSPParser")); +} + +nsCSPParser::~nsCSPParser() +{ + CSPPARSERLOG(("nsCSPParser::~nsCSPParser")); +} + +static bool +isCharacterToken(char16_t aSymbol) +{ + return (aSymbol >= 'a' && aSymbol <= 'z') || + (aSymbol >= 'A' && aSymbol <= 'Z'); +} + +static bool +isNumberToken(char16_t aSymbol) +{ + return (aSymbol >= '0' && aSymbol <= '9'); +} + +static bool +isValidHexDig(char16_t aHexDig) +{ + return (isNumberToken(aHexDig) || + (aHexDig >= 'A' && aHexDig <= 'F') || + (aHexDig >= 'a' && aHexDig <= 'f')); +} + +void +nsCSPParser::resetCurChar(const nsAString& aToken) +{ + mCurChar = aToken.BeginReading(); + mEndChar = aToken.EndReading(); + resetCurValue(); +} + +// The path is terminated by the first question mark ("?") or +// number sign ("#") character, or by the end of the URI. +// http://tools.ietf.org/html/rfc3986#section-3.3 +bool +nsCSPParser::atEndOfPath() +{ + return (atEnd() || peek(QUESTIONMARK) || peek(NUMBER_SIGN)); +} + +// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +bool +nsCSPParser::atValidUnreservedChar() +{ + return (peek(isCharacterToken) || peek(isNumberToken) || + peek(DASH) || peek(DOT) || + peek(UNDERLINE) || peek(TILDE)); +} + +// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" +// / "*" / "+" / "," / ";" / "=" +// Please note that even though ',' and ';' appear to be +// valid sub-delims according to the RFC production of paths, +// both can not appear here by itself, they would need to be +// pct-encoded in order to be part of the path. +bool +nsCSPParser::atValidSubDelimChar() +{ + return (peek(EXCLAMATION) || peek(DOLLAR) || peek(AMPERSAND) || + peek(SINGLEQUOTE) || peek(OPENBRACE) || peek(CLOSINGBRACE) || + peek(WILDCARD) || peek(PLUS) || peek(EQUALS)); +} + +// pct-encoded = "%" HEXDIG HEXDIG +bool +nsCSPParser::atValidPctEncodedChar() +{ + const char16_t* pctCurChar = mCurChar; + + if ((pctCurChar + 2) >= mEndChar) { + // string too short, can't be a valid pct-encoded char. + return false; + } + + // Any valid pct-encoding must follow the following format: + // "% HEXDIG HEXDIG" + if (PERCENT_SIGN != *pctCurChar || + !isValidHexDig(*(pctCurChar+1)) || + !isValidHexDig(*(pctCurChar+2))) { + return false; + } + return true; +} + +// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +// http://tools.ietf.org/html/rfc3986#section-3.3 +bool +nsCSPParser::atValidPathChar() +{ + return (atValidUnreservedChar() || + atValidSubDelimChar() || + atValidPctEncodedChar() || + peek(COLON) || peek(ATSYMBOL)); +} + +void +nsCSPParser::logWarningErrorToConsole(uint32_t aSeverityFlag, + const char* aProperty, + const char16_t* aParams[], + uint32_t aParamsLength) +{ + CSPPARSERLOG(("nsCSPParser::logWarningErrorToConsole: %s", aProperty)); + // send console messages off to the context and let the context + // deal with it (potentially messages need to be queued up) + mCSPContext->logToConsole(NS_ConvertUTF8toUTF16(aProperty).get(), + aParams, + aParamsLength, + EmptyString(), // aSourceName + EmptyString(), // aSourceLine + 0, // aLineNumber + 0, // aColumnNumber + aSeverityFlag); // aFlags +} + +bool +nsCSPParser::hostChar() +{ + if (atEnd()) { + return false; + } + return accept(isCharacterToken) || + accept(isNumberToken) || + accept(DASH); +} + +// (ALPHA / DIGIT / "+" / "-" / "." ) +bool +nsCSPParser::schemeChar() +{ + if (atEnd()) { + return false; + } + return accept(isCharacterToken) || + accept(isNumberToken) || + accept(PLUS) || + accept(DASH) || + accept(DOT); +} + +// port = ":" ( 1*DIGIT / "*" ) +bool +nsCSPParser::port() +{ + CSPPARSERLOG(("nsCSPParser::port, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + // Consume the COLON we just peeked at in houstSource + accept(COLON); + + // Resetting current value since we start to parse a port now. + // e.g; "http://www.example.com:8888" then we have already parsed + // everything up to (including) ":"; + resetCurValue(); + + // Port might be "*" + if (accept(WILDCARD)) { + return true; + } + + // Port must start with a number + if (!accept(isNumberToken)) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "couldntParsePort", + params, ArrayLength(params)); + return false; + } + // Consume more numbers and set parsed port to the nsCSPHost + while (accept(isNumberToken)) { /* consume */ } + return true; +} + +bool +nsCSPParser::subPath(nsCSPHostSrc* aCspHost) +{ + CSPPARSERLOG(("nsCSPParser::subPath, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + // Emergency exit to avoid endless loops in case a path in a CSP policy + // is longer than 512 characters, or also to avoid endless loops + // in case we are parsing unrecognized characters in the following loop. + uint32_t charCounter = 0; + nsString pctDecodedSubPath; + + while (!atEndOfPath()) { + if (peek(SLASH)) { + // before appendig any additional portion of a subpath we have to pct-decode + // that portion of the subpath. atValidPathChar() already verified a correct + // pct-encoding, now we can safely decode and append the decoded-sub path. + CSP_PercentDecodeStr(mCurValue, pctDecodedSubPath); + aCspHost->appendPath(pctDecodedSubPath); + // Resetting current value since we are appending parts of the path + // to aCspHost, e.g; "http://www.example.com/path1/path2" then the + // first part is "/path1", second part "/path2" + resetCurValue(); + } + else if (!atValidPathChar()) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "couldntParseInvalidSource", + params, ArrayLength(params)); + return false; + } + // potentially we have encountred a valid pct-encoded character in atValidPathChar(); + // if so, we have to account for "% HEXDIG HEXDIG" and advance the pointer past + // the pct-encoded char. + if (peek(PERCENT_SIGN)) { + advance(); + advance(); + } + advance(); + if (++charCounter > kSubHostPathCharacterCutoff) { + return false; + } + } + // before appendig any additional portion of a subpath we have to pct-decode + // that portion of the subpath. atValidPathChar() already verified a correct + // pct-encoding, now we can safely decode and append the decoded-sub path. + CSP_PercentDecodeStr(mCurValue, pctDecodedSubPath); + aCspHost->appendPath(pctDecodedSubPath); + resetCurValue(); + return true; +} + +bool +nsCSPParser::path(nsCSPHostSrc* aCspHost) +{ + CSPPARSERLOG(("nsCSPParser::path, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + // Resetting current value and forgetting everything we have parsed so far + // e.g. parsing "http://www.example.com/path1/path2", then + // "http://www.example.com" has already been parsed so far + // forget about it. + resetCurValue(); + + if (!accept(SLASH)) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "couldntParseInvalidSource", + params, ArrayLength(params)); + return false; + } + if (atEndOfPath()) { + // one slash right after host [port] is also considered a path, e.g. + // www.example.com/ should result in www.example.com/ + // please note that we do not have to perform any pct-decoding here + // because we are just appending a '/' and not any actual chars. + aCspHost->appendPath(NS_LITERAL_STRING("/")); + return true; + } + // path can begin with "/" but not "//" + // see http://tools.ietf.org/html/rfc3986#section-3.3 + if (peek(SLASH)) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "couldntParseInvalidSource", + params, ArrayLength(params)); + return false; + } + return subPath(aCspHost); +} + +bool +nsCSPParser::subHost() +{ + CSPPARSERLOG(("nsCSPParser::subHost, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + // Emergency exit to avoid endless loops in case a host in a CSP policy + // is longer than 512 characters, or also to avoid endless loops + // in case we are parsing unrecognized characters in the following loop. + uint32_t charCounter = 0; + + while (!atEndOfPath() && !peek(COLON) && !peek(SLASH)) { + ++charCounter; + while (hostChar()) { + /* consume */ + ++charCounter; + } + if (accept(DOT) && !hostChar()) { + return false; + } + if (charCounter > kSubHostPathCharacterCutoff) { + return false; + } + } + return true; +} + +// host = "*" / [ "*." ] 1*host-char *( "." 1*host-char ) +nsCSPHostSrc* +nsCSPParser::host() +{ + CSPPARSERLOG(("nsCSPParser::host, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + // Check if the token starts with "*"; please remember that we handle + // a single "*" as host in sourceExpression, but we still have to handle + // the case where a scheme was defined, e.g., as: + // "https://*", "*.example.com", "*:*", etc. + if (accept(WILDCARD)) { + // Might solely be the wildcard + if (atEnd() || peek(COLON)) { + return new nsCSPHostSrc(mCurValue); + } + // If the token is not only the "*", a "." must follow right after + if (!accept(DOT)) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "couldntParseInvalidHost", + params, ArrayLength(params)); + return nullptr; + } + } + + // Expecting at least one host-char + if (!hostChar()) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "couldntParseInvalidHost", + params, ArrayLength(params)); + return nullptr; + } + + // There might be several sub hosts defined. + if (!subHost()) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "couldntParseInvalidHost", + params, ArrayLength(params)); + return nullptr; + } + + // HostName might match a keyword, log to the console. + if (CSP_IsQuotelessKeyword(mCurValue)) { + nsString keyword = mCurValue; + ToLowerCase(keyword); + const char16_t* params[] = { mCurToken.get(), keyword.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "hostNameMightBeKeyword", + params, ArrayLength(params)); + } + + // Create a new nsCSPHostSrc with the parsed host. + return new nsCSPHostSrc(mCurValue); +} + +// apps use special hosts; "app://{app-host-is-uid}"" +nsCSPHostSrc* +nsCSPParser::appHost() +{ + CSPPARSERLOG(("nsCSPParser::appHost, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + while (hostChar()) { /* consume */ } + + // appHosts have to end with "}", otherwise we have to report an error + if (!accept(CLOSE_CURL)) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "couldntParseInvalidSource", + params, ArrayLength(params)); + return nullptr; + } + return new nsCSPHostSrc(mCurValue); +} + +// keyword-source = "'self'" / "'unsafe-inline'" / "'unsafe-eval'" +nsCSPBaseSrc* +nsCSPParser::keywordSource() +{ + CSPPARSERLOG(("nsCSPParser::keywordSource, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + // Special case handling for 'self' which is not stored internally as a keyword, + // but rather creates a nsCSPHostSrc using the selfURI + if (CSP_IsKeyword(mCurToken, CSP_SELF)) { + return CSP_CreateHostSrcFromURI(mSelfURI); + } + + if (CSP_IsKeyword(mCurToken, CSP_STRICT_DYNAMIC)) { + // make sure strict dynamic is enabled + if (!sStrictDynamicEnabled) { + return nullptr; + } + if (!CSP_IsDirective(mCurDir[0], nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE)) { + // Todo: Enforce 'strict-dynamic' within default-src; see Bug 1313937 + const char16_t* params[] = { u"strict-dynamic" }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringStrictDynamic", + params, ArrayLength(params)); + return nullptr; + } + mStrictDynamic = true; + return new nsCSPKeywordSrc(CSP_KeywordToEnum(mCurToken)); + } + + if (CSP_IsKeyword(mCurToken, CSP_UNSAFE_INLINE)) { + nsWeakPtr ctx = mCSPContext->GetLoadingContext(); + nsCOMPtr<nsIDocument> doc = do_QueryReferent(ctx); + if (doc) { + doc->SetHasUnsafeInlineCSP(true); + } + // make sure script-src only contains 'unsafe-inline' once; + // ignore duplicates and log warning + if (mUnsafeInlineKeywordSrc) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringDuplicateSrc", + params, ArrayLength(params)); + return nullptr; + } + // cache if we encounter 'unsafe-inline' so we can invalidate (ignore) it in + // case that script-src directive also contains hash- or nonce-. + mUnsafeInlineKeywordSrc = new nsCSPKeywordSrc(CSP_KeywordToEnum(mCurToken)); + return mUnsafeInlineKeywordSrc; + } + + if (CSP_IsKeyword(mCurToken, CSP_UNSAFE_EVAL)) { + nsWeakPtr ctx = mCSPContext->GetLoadingContext(); + nsCOMPtr<nsIDocument> doc = do_QueryReferent(ctx); + if (doc) { + doc->SetHasUnsafeEvalCSP(true); + } + return new nsCSPKeywordSrc(CSP_KeywordToEnum(mCurToken)); + } + return nullptr; +} + +// host-source = [ scheme "://" ] host [ port ] [ path ] +nsCSPHostSrc* +nsCSPParser::hostSource() +{ + CSPPARSERLOG(("nsCSPParser::hostSource, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + // Special case handling for app specific hosts + if (accept(OPEN_CURL)) { + // If appHost() returns null, the error was handled in appHost(). + // appHosts can not have a port, or path, we can return. + return appHost(); + } + + nsCSPHostSrc* cspHost = host(); + if (!cspHost) { + // Error was reported in host() + return nullptr; + } + + // Calling port() to see if there is a port to parse, if an error + // occurs, port() reports the error, if port() returns true; + // we have a valid port, so we add it to cspHost. + if (peek(COLON)) { + if (!port()) { + delete cspHost; + return nullptr; + } + cspHost->setPort(mCurValue); + } + + if (atEndOfPath()) { + return cspHost; + } + + // Calling path() to see if there is a path to parse, if an error + // occurs, path() reports the error; handing cspHost as an argument + // which simplifies parsing of several paths. + if (!path(cspHost)) { + // If the host [port] is followed by a path, it has to be a valid path, + // otherwise we pass the nullptr, indicating an error, up the callstack. + // see also http://www.w3.org/TR/CSP11/#source-list + delete cspHost; + return nullptr; + } + return cspHost; +} + +// scheme-source = scheme ":" +nsCSPSchemeSrc* +nsCSPParser::schemeSource() +{ + CSPPARSERLOG(("nsCSPParser::schemeSource, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + if (!accept(isCharacterToken)) { + return nullptr; + } + while (schemeChar()) { /* consume */ } + nsString scheme = mCurValue; + + // If the potential scheme is not followed by ":" - it's not a scheme + if (!accept(COLON)) { + return nullptr; + } + + // If the chraracter following the ":" is a number or the "*" + // then we are not parsing a scheme; but rather a host; + if (peek(isNumberToken) || peek(WILDCARD)) { + return nullptr; + } + + return new nsCSPSchemeSrc(scheme); +} + +// nonce-source = "'nonce-" nonce-value "'" +nsCSPNonceSrc* +nsCSPParser::nonceSource() +{ + CSPPARSERLOG(("nsCSPParser::nonceSource, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + // Check if mCurToken begins with "'nonce-" and ends with "'" + if (!StringBeginsWith(mCurToken, NS_ConvertUTF8toUTF16(CSP_EnumToKeyword(CSP_NONCE)), + nsASCIICaseInsensitiveStringComparator()) || + mCurToken.Last() != SINGLEQUOTE) { + return nullptr; + } + + // Trim surrounding single quotes + const nsAString& expr = Substring(mCurToken, 1, mCurToken.Length() - 2); + + int32_t dashIndex = expr.FindChar(DASH); + if (dashIndex < 0) { + return nullptr; + } + // cache if encountering hash or nonce to invalidate unsafe-inline + mHasHashOrNonce = true; + return new nsCSPNonceSrc(Substring(expr, + dashIndex + 1, + expr.Length() - dashIndex + 1)); +} + +// hash-source = "'" hash-algo "-" base64-value "'" +nsCSPHashSrc* +nsCSPParser::hashSource() +{ + CSPPARSERLOG(("nsCSPParser::hashSource, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + // Check if mCurToken starts and ends with "'" + if (mCurToken.First() != SINGLEQUOTE || + mCurToken.Last() != SINGLEQUOTE) { + return nullptr; + } + + // Trim surrounding single quotes + const nsAString& expr = Substring(mCurToken, 1, mCurToken.Length() - 2); + + int32_t dashIndex = expr.FindChar(DASH); + if (dashIndex < 0) { + return nullptr; + } + + nsAutoString algo(Substring(expr, 0, dashIndex)); + nsAutoString hash(Substring(expr, dashIndex + 1, expr.Length() - dashIndex + 1)); + + for (uint32_t i = 0; i < kHashSourceValidFnsLen; i++) { + if (algo.LowerCaseEqualsASCII(kHashSourceValidFns[i])) { + // cache if encountering hash or nonce to invalidate unsafe-inline + mHasHashOrNonce = true; + return new nsCSPHashSrc(algo, hash); + } + } + return nullptr; +} + +// source-expression = scheme-source / host-source / keyword-source +// / nonce-source / hash-source +nsCSPBaseSrc* +nsCSPParser::sourceExpression() +{ + CSPPARSERLOG(("nsCSPParser::sourceExpression, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + // Check if it is a keyword + if (nsCSPBaseSrc *cspKeyword = keywordSource()) { + return cspKeyword; + } + + // Check if it is a nonce-source + if (nsCSPNonceSrc* cspNonce = nonceSource()) { + return cspNonce; + } + + // Check if it is a hash-source + if (nsCSPHashSrc* cspHash = hashSource()) { + return cspHash; + } + + // We handle a single "*" as host here, to avoid any confusion when applying the default scheme. + // However, we still would need to apply the default scheme in case we would parse "*:80". + if (mCurToken.EqualsASCII("*")) { + return new nsCSPHostSrc(NS_LITERAL_STRING("*")); + } + + // Calling resetCurChar allows us to use mCurChar and mEndChar + // to parse mCurToken; e.g. mCurToken = "http://www.example.com", then + // mCurChar = 'h' + // mEndChar = points just after the last 'm' + // mCurValue = "" + resetCurChar(mCurToken); + + // Check if mCurToken starts with a scheme + nsAutoString parsedScheme; + if (nsCSPSchemeSrc* cspScheme = schemeSource()) { + // mCurToken might only enforce a specific scheme + if (atEnd()) { + return cspScheme; + } + // If something follows the scheme, we do not create + // a nsCSPSchemeSrc, but rather a nsCSPHostSrc, which + // needs to know the scheme to enforce; remember the + // scheme and delete cspScheme; + cspScheme->toString(parsedScheme); + parsedScheme.Trim(":", false, true); + delete cspScheme; + + // If mCurToken provides not only a scheme, but also a host, we have to check + // if two slashes follow the scheme. + if (!accept(SLASH) || !accept(SLASH)) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "failedToParseUnrecognizedSource", + params, ArrayLength(params)); + return nullptr; + } + } + + // Calling resetCurValue allows us to keep pointers for mCurChar and mEndChar + // alive, but resets mCurValue; e.g. mCurToken = "http://www.example.com", then + // mCurChar = 'w' + // mEndChar = 'm' + // mCurValue = "" + resetCurValue(); + + // If mCurToken does not provide a scheme (scheme-less source), we apply the scheme + // from selfURI + if (parsedScheme.IsEmpty()) { + // Resetting internal helpers, because we might already have parsed some of the host + // when trying to parse a scheme. + resetCurChar(mCurToken); + nsAutoCString selfScheme; + mSelfURI->GetScheme(selfScheme); + parsedScheme.AssignASCII(selfScheme.get()); + } + + // At this point we are expecting a host to be parsed. + // Trying to create a new nsCSPHost. + if (nsCSPHostSrc *cspHost = hostSource()) { + // Do not forget to set the parsed scheme. + cspHost->setScheme(parsedScheme); + return cspHost; + } + // Error was reported in hostSource() + return nullptr; +} + +// source-list = *WSP [ source-expression *( 1*WSP source-expression ) *WSP ] +// / *WSP "'none'" *WSP +void +nsCSPParser::sourceList(nsTArray<nsCSPBaseSrc*>& outSrcs) +{ + bool isNone = false; + + // remember, srcs start at index 1 + for (uint32_t i = 1; i < mCurDir.Length(); i++) { + // mCurToken is only set here and remains the current token + // to be processed, which avoid passing arguments between functions. + mCurToken = mCurDir[i]; + resetCurValue(); + + CSPPARSERLOG(("nsCSPParser::sourceList, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + // Special case handling for none: + // Ignore 'none' if any other src is available. + // (See http://www.w3.org/TR/CSP11/#parsing) + if (CSP_IsKeyword(mCurToken, CSP_NONE)) { + isNone = true; + continue; + } + // Must be a regular source expression + nsCSPBaseSrc* src = sourceExpression(); + if (src) { + outSrcs.AppendElement(src); + } + } + + // Check if the directive contains a 'none' + if (isNone) { + // If the directive contains no other srcs, then we set the 'none' + if (outSrcs.Length() == 0) { + nsCSPKeywordSrc *keyword = new nsCSPKeywordSrc(CSP_NONE); + outSrcs.AppendElement(keyword); + } + // Otherwise, we ignore 'none' and report a warning + else { + NS_ConvertUTF8toUTF16 unicodeNone(CSP_EnumToKeyword(CSP_NONE)); + const char16_t* params[] = { unicodeNone.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringUnknownOption", + params, ArrayLength(params)); + } + } +} + +void +nsCSPParser::referrerDirectiveValue(nsCSPDirective* aDir) +{ + // directive-value = "none" / "none-when-downgrade" / "origin" / "origin-when-cross-origin" / "unsafe-url" + // directive name is token 0, we need to examine the remaining tokens (and + // there should only be one token in the value). + CSPPARSERLOG(("nsCSPParser::referrerDirectiveValue")); + + if (mCurDir.Length() != 2) { + CSPPARSERLOG(("Incorrect number of tokens in referrer directive, got %d expected 1", + mCurDir.Length() - 1)); + delete aDir; + return; + } + + if (!mozilla::net::IsValidReferrerPolicy(mCurDir[1])) { + CSPPARSERLOG(("invalid value for referrer directive: %s", + NS_ConvertUTF16toUTF8(mCurDir[1]).get())); + delete aDir; + return; + } + + //referrer-directive deprecation warning + const char16_t* params[] = { mCurDir[1].get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "deprecatedReferrerDirective", + params, ArrayLength(params)); + + // the referrer policy is valid, so go ahead and use it. + mPolicy->setReferrerPolicy(&mCurDir[1]); + mPolicy->addDirective(aDir); +} + +void +nsCSPParser::requireSRIForDirectiveValue(nsRequireSRIForDirective* aDir) +{ + CSPPARSERLOG(("nsCSPParser::requireSRIForDirectiveValue")); + + // directive-value = "style" / "script" + // directive name is token 0, we need to examine the remaining tokens + for (uint32_t i = 1; i < mCurDir.Length(); i++) { + // mCurToken is only set here and remains the current token + // to be processed, which avoid passing arguments between functions. + mCurToken = mCurDir[i]; + resetCurValue(); + CSPPARSERLOG(("nsCSPParser:::directive (require-sri-for directive), " + "mCurToken: %s (valid), mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + // add contentPolicyTypes to the CSP's required-SRI list for this token + if (mCurToken.LowerCaseEqualsASCII(kScript)) { + aDir->addType(nsIContentPolicy::TYPE_SCRIPT); + } + else if (mCurToken.LowerCaseEqualsASCII(kStyle)) { + aDir->addType(nsIContentPolicy::TYPE_STYLESHEET); + } else { + const char16_t* invalidTokenName[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "failedToParseUnrecognizedSource", + invalidTokenName, ArrayLength(invalidTokenName)); + CSPPARSERLOG(("nsCSPParser:::directive (require-sri-for directive), " + "mCurToken: %s (invalid), mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + } + } + + if (!(aDir->hasType(nsIContentPolicy::TYPE_STYLESHEET)) && + !(aDir->hasType(nsIContentPolicy::TYPE_SCRIPT))) { + const char16_t* directiveName[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringDirectiveWithNoValues", + directiveName, ArrayLength(directiveName)); + delete aDir; + return; + } + + mPolicy->addDirective(aDir); +} + +void +nsCSPParser::reportURIList(nsCSPDirective* aDir) +{ + CSPPARSERLOG(("nsCSPParser::reportURIList")); + + nsTArray<nsCSPBaseSrc*> srcs; + nsCOMPtr<nsIURI> uri; + nsresult rv; + + // remember, srcs start at index 1 + for (uint32_t i = 1; i < mCurDir.Length(); i++) { + mCurToken = mCurDir[i]; + + CSPPARSERLOG(("nsCSPParser::reportURIList, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + rv = NS_NewURI(getter_AddRefs(uri), mCurToken, "", mSelfURI); + + // If creating the URI casued an error, skip this URI + if (NS_FAILED(rv)) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "couldNotParseReportURI", + params, ArrayLength(params)); + continue; + } + + // Create new nsCSPReportURI and append to the list. + nsCSPReportURI* reportURI = new nsCSPReportURI(uri); + srcs.AppendElement(reportURI); + } + + if (srcs.Length() == 0) { + const char16_t* directiveName[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringDirectiveWithNoValues", + directiveName, ArrayLength(directiveName)); + delete aDir; + return; + } + + aDir->addSrcs(srcs); + mPolicy->addDirective(aDir); +} + +/* Helper function for parsing sandbox flags. This function solely concatenates + * all the source list tokens (the sandbox flags) so the attribute parser + * (nsContentUtils::ParseSandboxAttributeToFlags) can parse them. + */ +void +nsCSPParser::sandboxFlagList(nsCSPDirective* aDir) +{ + CSPPARSERLOG(("nsCSPParser::sandboxFlagList")); + + nsAutoString flags; + + // remember, srcs start at index 1 + for (uint32_t i = 1; i < mCurDir.Length(); i++) { + mCurToken = mCurDir[i]; + + CSPPARSERLOG(("nsCSPParser::sandboxFlagList, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + if (!nsContentUtils::IsValidSandboxFlag(mCurToken)) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "couldntParseInvalidSandboxFlag", + params, ArrayLength(params)); + continue; + } + + flags.Append(mCurToken); + if (i != mCurDir.Length() - 1) { + flags.AppendASCII(" "); + } + } + + // Please note that the sandbox directive can exist + // by itself (not containing any flags). + nsTArray<nsCSPBaseSrc*> srcs; + srcs.AppendElement(new nsCSPSandboxFlags(flags)); + aDir->addSrcs(srcs); + mPolicy->addDirective(aDir); +} + +// directive-value = *( WSP / <VCHAR except ";" and ","> ) +void +nsCSPParser::directiveValue(nsTArray<nsCSPBaseSrc*>& outSrcs) +{ + CSPPARSERLOG(("nsCSPParser::directiveValue")); + + // Just forward to sourceList + sourceList(outSrcs); +} + +// directive-name = 1*( ALPHA / DIGIT / "-" ) +nsCSPDirective* +nsCSPParser::directiveName() +{ + CSPPARSERLOG(("nsCSPParser::directiveName, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + // Check if it is a valid directive + if (!CSP_IsValidDirective(mCurToken) || + (!sCSPExperimentalEnabled && + CSP_IsDirective(mCurToken, nsIContentSecurityPolicy::REQUIRE_SRI_FOR))) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "couldNotProcessUnknownDirective", + params, ArrayLength(params)); + return nullptr; + } + + // The directive 'reflected-xss' is part of CSP 1.1, see: + // http://www.w3.org/TR/2014/WD-CSP11-20140211/#reflected-xss + // Currently we are not supporting that directive, hence we log a + // warning to the console and ignore the directive including its values. + if (CSP_IsDirective(mCurToken, nsIContentSecurityPolicy::REFLECTED_XSS_DIRECTIVE)) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "notSupportingDirective", + params, ArrayLength(params)); + return nullptr; + } + + // Make sure the directive does not already exist + // (see http://www.w3.org/TR/CSP11/#parsing) + if (mPolicy->hasDirective(CSP_StringToCSPDirective(mCurToken))) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "duplicateDirective", + params, ArrayLength(params)); + return nullptr; + } + + // CSP delivered via meta tag should ignore the following directives: + // report-uri, frame-ancestors, and sandbox, see: + // http://www.w3.org/TR/CSP11/#delivery-html-meta-element + if (mDeliveredViaMetaTag && + ((CSP_IsDirective(mCurToken, nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE)) || + (CSP_IsDirective(mCurToken, nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE)) || + (CSP_IsDirective(mCurToken, nsIContentSecurityPolicy::SANDBOX_DIRECTIVE)))) { + // log to the console to indicate that meta CSP is ignoring the directive + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "ignoringSrcFromMetaCSP", + params, ArrayLength(params)); + return nullptr; + } + + // special case handling for block-all-mixed-content + if (CSP_IsDirective(mCurToken, nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT)) { + return new nsBlockAllMixedContentDirective(CSP_StringToCSPDirective(mCurToken)); + } + + // special case handling for upgrade-insecure-requests + if (CSP_IsDirective(mCurToken, nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE)) { + return new nsUpgradeInsecureDirective(CSP_StringToCSPDirective(mCurToken)); + } + + // child-src has it's own class to handle frame-src if necessary + if (CSP_IsDirective(mCurToken, nsIContentSecurityPolicy::CHILD_SRC_DIRECTIVE)) { + mChildSrc = new nsCSPChildSrcDirective(CSP_StringToCSPDirective(mCurToken)); + return mChildSrc; + } + + // if we have a frame-src, cache it so we can decide whether to use child-src + if (CSP_IsDirective(mCurToken, nsIContentSecurityPolicy::FRAME_SRC_DIRECTIVE)) { + const char16_t* params[] = { mCurToken.get(), NS_LITERAL_STRING("child-src").get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "deprecatedDirective", + params, ArrayLength(params)); + mFrameSrc = new nsCSPDirective(CSP_StringToCSPDirective(mCurToken)); + return mFrameSrc; + } + + if (CSP_IsDirective(mCurToken, nsIContentSecurityPolicy::REQUIRE_SRI_FOR)) { + return new nsRequireSRIForDirective(CSP_StringToCSPDirective(mCurToken)); + } + + return new nsCSPDirective(CSP_StringToCSPDirective(mCurToken)); +} + +// directive = *WSP [ directive-name [ WSP directive-value ] ] +void +nsCSPParser::directive() +{ + // Set the directiveName to mCurToken + // Remember, the directive name is stored at index 0 + mCurToken = mCurDir[0]; + + CSPPARSERLOG(("nsCSPParser::directive, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + + // Make sure that the directive-srcs-array contains at least + // one directive and one src. + if (mCurDir.Length() < 1) { + const char16_t* params[] = { u"directive missing" }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "failedToParseUnrecognizedSource", + params, ArrayLength(params)); + return; + } + + // Try to create a new CSPDirective + nsCSPDirective* cspDir = directiveName(); + if (!cspDir) { + // if we can not create a CSPDirective, we can skip parsing the srcs for that array + return; + } + + // special case handling for block-all-mixed-content, which is only specified + // by a directive name but does not include any srcs. + if (cspDir->equals(nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT)) { + if (mCurDir.Length() > 1) { + const char16_t* params[] = { u"block-all-mixed-content" }; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "ignoreSrcForDirective", + params, ArrayLength(params)); + } + // add the directive and return + mPolicy->addDirective(cspDir); + return; + } + + // special case handling for upgrade-insecure-requests, which is only specified + // by a directive name but does not include any srcs. + if (cspDir->equals(nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE)) { + if (mCurDir.Length() > 1) { + const char16_t* params[] = { u"upgrade-insecure-requests" }; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "ignoreSrcForDirective", + params, ArrayLength(params)); + } + // add the directive and return + mPolicy->addUpgradeInsecDir(static_cast<nsUpgradeInsecureDirective*>(cspDir)); + return; + } + + // special case handling for require-sri-for, which has directive values that + // are well-defined tokens but are not sources + if (cspDir->equals(nsIContentSecurityPolicy::REQUIRE_SRI_FOR)) { + requireSRIForDirectiveValue(static_cast<nsRequireSRIForDirective*>(cspDir)); + return; + } + + // special case handling of the referrer directive (since it doesn't contain + // source lists) + if (cspDir->equals(nsIContentSecurityPolicy::REFERRER_DIRECTIVE)) { + referrerDirectiveValue(cspDir); + return; + } + + // special case handling for report-uri directive (since it doesn't contain + // a valid source list but rather actual URIs) + if (CSP_IsDirective(mCurDir[0], nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE)) { + reportURIList(cspDir); + return; + } + + // special case handling for sandbox directive (since it doe4sn't contain + // a valid source list but rather special sandbox flags) + if (CSP_IsDirective(mCurDir[0], nsIContentSecurityPolicy::SANDBOX_DIRECTIVE)) { + sandboxFlagList(cspDir); + return; + } + + // make sure to reset cache variables when trying to invalidate unsafe-inline; + // unsafe-inline might not only appear in script-src, but also in default-src + mHasHashOrNonce = false; + mStrictDynamic = false; + mUnsafeInlineKeywordSrc = nullptr; + + // Try to parse all the srcs by handing the array off to directiveValue + nsTArray<nsCSPBaseSrc*> srcs; + directiveValue(srcs); + + // If we can not parse any srcs; we let the source expression be the empty set ('none') + // see, http://www.w3.org/TR/CSP11/#source-list-parsing + if (srcs.Length() == 0) { + nsCSPKeywordSrc *keyword = new nsCSPKeywordSrc(CSP_NONE); + srcs.AppendElement(keyword); + } + + // If policy contains 'strict-dynamic' invalidate all srcs within script-src. + if (mStrictDynamic) { + MOZ_ASSERT(cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE), + "strict-dynamic only allowed within script-src"); + for (uint32_t i = 0; i < srcs.Length(); i++) { + // Please note that nsCSPNonceSrc as well as nsCSPHashSrc overwrite invalidate(), + // so it's fine to just call invalidate() on all srcs. Please also note that + // nsCSPKeywordSrc() can not be invalidated and always returns false unless the + // keyword is 'strict-dynamic' in which case we allow the load if the script is + // not parser created! + srcs[i]->invalidate(); + // Log a message to the console that src will be ignored. + nsAutoString srcStr; + srcs[i]->toString(srcStr); + // Even though we invalidate all of the srcs internally, we don't want to log + // messages for the srcs: (1) strict-dynamic, (2) unsafe-inline, + // (3) nonces, and (4) hashes + if (!srcStr.EqualsASCII(CSP_EnumToKeyword(CSP_STRICT_DYNAMIC)) && + !srcStr.EqualsASCII(CSP_EnumToKeyword(CSP_UNSAFE_EVAL)) && + !StringBeginsWith(NS_ConvertUTF16toUTF8(srcStr), NS_LITERAL_CSTRING("'nonce-")) && + !StringBeginsWith(NS_ConvertUTF16toUTF8(srcStr), NS_LITERAL_CSTRING("'sha"))) + { + const char16_t* params[] = { srcStr.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringSrcForStrictDynamic", + params, ArrayLength(params)); + } + } + // Log a warning that all scripts might be blocked because the policy contains + // 'strict-dynamic' but no valid nonce or hash. + if (!mHasHashOrNonce) { + const char16_t* params[] = { mCurDir[0].get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "strictDynamicButNoHashOrNonce", + params, ArrayLength(params)); + } + } + else if (mHasHashOrNonce && mUnsafeInlineKeywordSrc && + (cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) || + cspDir->equals(nsIContentSecurityPolicy::STYLE_SRC_DIRECTIVE))) { + mUnsafeInlineKeywordSrc->invalidate(); + // log to the console that unsafe-inline will be ignored + const char16_t* params[] = { u"'unsafe-inline'" }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringSrcWithinScriptStyleSrc", + params, ArrayLength(params)); + } + + // Add the newly created srcs to the directive and add the directive to the policy + cspDir->addSrcs(srcs); + mPolicy->addDirective(cspDir); +} + +// policy = [ directive *( ";" [ directive ] ) ] +nsCSPPolicy* +nsCSPParser::policy() +{ + CSPPARSERLOG(("nsCSPParser::policy")); + + mPolicy = new nsCSPPolicy(); + for (uint32_t i = 0; i < mTokens.Length(); i++) { + // All input is already tokenized; set one tokenized array in the form of + // [ name, src, src, ... ] + // to mCurDir and call directive which processes the current directive. + mCurDir = mTokens[i]; + directive(); + } + + if (mChildSrc && !mFrameSrc) { + // if we have a child-src, it handles frame-src too, unless frame-src is set + mChildSrc->setHandleFrameSrc(); + } + + return mPolicy; +} + +nsCSPPolicy* +nsCSPParser::parseContentSecurityPolicy(const nsAString& aPolicyString, + nsIURI *aSelfURI, + bool aReportOnly, + nsCSPContext* aCSPContext, + bool aDeliveredViaMetaTag) +{ + if (CSPPARSERLOGENABLED()) { + CSPPARSERLOG(("nsCSPParser::parseContentSecurityPolicy, policy: %s", + NS_ConvertUTF16toUTF8(aPolicyString).get())); + CSPPARSERLOG(("nsCSPParser::parseContentSecurityPolicy, selfURI: %s", + aSelfURI->GetSpecOrDefault().get())); + CSPPARSERLOG(("nsCSPParser::parseContentSecurityPolicy, reportOnly: %s", + (aReportOnly ? "true" : "false"))); + CSPPARSERLOG(("nsCSPParser::parseContentSecurityPolicy, deliveredViaMetaTag: %s", + (aDeliveredViaMetaTag ? "true" : "false"))); + } + + NS_ASSERTION(aSelfURI, "Can not parseContentSecurityPolicy without aSelfURI"); + + // Separate all input into tokens and store them in the form of: + // [ [ name, src, src, ... ], [ name, src, src, ... ], ... ] + // The tokenizer itself can not fail; all eventual errors + // are detected in the parser itself. + + nsTArray< nsTArray<nsString> > tokens; + nsCSPTokenizer::tokenizeCSPPolicy(aPolicyString, tokens); + + nsCSPParser parser(tokens, aSelfURI, aCSPContext, aDeliveredViaMetaTag); + + // Start the parser to generate a new CSPPolicy using the generated tokens. + nsCSPPolicy* policy = parser.policy(); + + // Check that report-only policies define a report-uri, otherwise log warning. + if (aReportOnly) { + policy->setReportOnlyFlag(true); + if (!policy->hasDirective(nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE)) { + nsAutoCString prePath; + nsresult rv = aSelfURI->GetPrePath(prePath); + NS_ENSURE_SUCCESS(rv, policy); + NS_ConvertUTF8toUTF16 unicodePrePath(prePath); + const char16_t* params[] = { unicodePrePath.get() }; + parser.logWarningErrorToConsole(nsIScriptError::warningFlag, "reportURInotInReportOnlyHeader", + params, ArrayLength(params)); + } + } + + if (policy->getNumDirectives() == 0) { + // Individual errors were already reported in the parser, but if + // we do not have an enforcable directive at all, we return null. + delete policy; + return nullptr; + } + + if (CSPPARSERLOGENABLED()) { + nsString parsedPolicy; + policy->toString(parsedPolicy); + CSPPARSERLOG(("nsCSPParser::parseContentSecurityPolicy, parsedPolicy: %s", + NS_ConvertUTF16toUTF8(parsedPolicy).get())); + } + + return policy; +} diff --git a/dom/security/nsCSPParser.h b/dom/security/nsCSPParser.h new file mode 100644 index 000000000..30954b10f --- /dev/null +++ b/dom/security/nsCSPParser.h @@ -0,0 +1,262 @@ +/* -*- 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 nsCSPParser_h___ +#define nsCSPParser_h___ + +#include "nsCSPUtils.h" +#include "nsIURI.h" +#include "nsString.h" + +/** + * How does the parsing work? + * + * We generate tokens by splitting the policy-string by whitespace and semicolon. + * Interally the tokens are represented as an array of string-arrays: + * + * [ + * [ name, src, src, src, ... ], + * [ name, src, src, src, ... ], + * [ name, src, src, src, ... ] + * ] + * + * for example: + * [ + * [ img-src, http://www.example.com, http:www.test.com ], + * [ default-src, 'self'], + * [ script-src, 'unsafe-eval', 'unsafe-inline' ], + * ] + * + * The first element of each array has to be a valid directive-name, otherwise we can + * ignore the remaining elements of the array. Also, if the + * directive already exists in the current policy, we can ignore + * the remaining elements of that array. (http://www.w3.org/TR/CSP/#parsing) + */ + +typedef nsTArray< nsTArray<nsString> > cspTokens; + +class nsCSPTokenizer { + + public: + static void tokenizeCSPPolicy(const nsAString &aPolicyString, cspTokens& outTokens); + + private: + nsCSPTokenizer(const char16_t* aStart, const char16_t* aEnd); + ~nsCSPTokenizer(); + + inline bool atEnd() + { + return mCurChar >= mEndChar; + } + + inline void skipWhiteSpace() + { + while (mCurChar < mEndChar && + nsContentUtils::IsHTMLWhitespace(*mCurChar)) { + mCurToken.Append(*mCurChar++); + } + mCurToken.Truncate(); + } + + inline void skipWhiteSpaceAndSemicolon() + { + while (mCurChar < mEndChar && (*mCurChar == ';' || + nsContentUtils::IsHTMLWhitespace(*mCurChar))){ + mCurToken.Append(*mCurChar++); + } + mCurToken.Truncate(); + } + + inline bool accept(char16_t aChar) + { + NS_ASSERTION(mCurChar < mEndChar, "Trying to dereference mEndChar"); + if (*mCurChar == aChar) { + mCurToken.Append(*mCurChar++); + return true; + } + return false; + } + + void generateNextToken(); + void generateTokens(cspTokens& outTokens); + + const char16_t* mCurChar; + const char16_t* mEndChar; + nsString mCurToken; +}; + + +class nsCSPParser { + + public: + /** + * The CSP parser only has one publicly accessible function, which is parseContentSecurityPolicy. + * Internally the input string is separated into string tokens and policy() is called, which starts + * parsing the policy. The parser calls one function after the other according the the source-list + * from http://www.w3.org/TR/CSP11/#source-list. E.g., the parser can only call port() after the parser + * has already processed any possible host in host(), similar to a finite state machine. + */ + static nsCSPPolicy* parseContentSecurityPolicy(const nsAString &aPolicyString, + nsIURI *aSelfURI, + bool aReportOnly, + nsCSPContext* aCSPContext, + bool aDeliveredViaMetaTag); + + private: + nsCSPParser(cspTokens& aTokens, + nsIURI* aSelfURI, + nsCSPContext* aCSPContext, + bool aDeliveredViaMetaTag); + + static bool sCSPExperimentalEnabled; + static bool sStrictDynamicEnabled; + + ~nsCSPParser(); + + + // Parsing the CSP using the source-list from http://www.w3.org/TR/CSP11/#source-list + nsCSPPolicy* policy(); + void directive(); + nsCSPDirective* directiveName(); + void directiveValue(nsTArray<nsCSPBaseSrc*>& outSrcs); + void requireSRIForDirectiveValue(nsRequireSRIForDirective* aDir); + void referrerDirectiveValue(nsCSPDirective* aDir); + void reportURIList(nsCSPDirective* aDir); + void sandboxFlagList(nsCSPDirective* aDir); + void sourceList(nsTArray<nsCSPBaseSrc*>& outSrcs); + nsCSPBaseSrc* sourceExpression(); + nsCSPSchemeSrc* schemeSource(); + nsCSPHostSrc* hostSource(); + nsCSPBaseSrc* keywordSource(); + nsCSPNonceSrc* nonceSource(); + nsCSPHashSrc* hashSource(); + nsCSPHostSrc* appHost(); // helper function to support app specific hosts + nsCSPHostSrc* host(); + bool hostChar(); + bool schemeChar(); + bool port(); + bool path(nsCSPHostSrc* aCspHost); + + bool subHost(); // helper function to parse subDomains + bool atValidUnreservedChar(); // helper function to parse unreserved + bool atValidSubDelimChar(); // helper function to parse sub-delims + bool atValidPctEncodedChar(); // helper function to parse pct-encoded + bool subPath(nsCSPHostSrc* aCspHost); // helper function to parse paths + + inline bool atEnd() + { + return mCurChar >= mEndChar; + } + + inline bool accept(char16_t aSymbol) + { + if (atEnd()) { return false; } + return (*mCurChar == aSymbol) && advance(); + } + + inline bool accept(bool (*aClassifier) (char16_t)) + { + if (atEnd()) { return false; } + return (aClassifier(*mCurChar)) && advance(); + } + + inline bool peek(char16_t aSymbol) + { + if (atEnd()) { return false; } + return *mCurChar == aSymbol; + } + + inline bool peek(bool (*aClassifier) (char16_t)) + { + if (atEnd()) { return false; } + return aClassifier(*mCurChar); + } + + inline bool advance() + { + if (atEnd()) { return false; } + mCurValue.Append(*mCurChar++); + return true; + } + + inline void resetCurValue() + { + mCurValue.Truncate(); + } + + bool atEndOfPath(); + bool atValidPathChar(); + + void resetCurChar(const nsAString& aToken); + + void logWarningErrorToConsole(uint32_t aSeverityFlag, + const char* aProperty, + const char16_t* aParams[], + uint32_t aParamsLength); + +/** + * When parsing the policy, the parser internally uses the following helper + * variables/members which are used/reset during parsing. The following + * example explains how they are used. + * The tokenizer separats all input into arrays of arrays of strings, which + * are stored in mTokens, for example: + * mTokens = [ [ script-src, http://www.example.com, 'self' ], ... ] + * + * When parsing starts, mCurdir always holds the currently processed array of strings. + * In our example: + * mCurDir = [ script-src, http://www.example.com, 'self' ] + * + * During parsing, we process/consume one string at a time of that array. + * We set mCurToken to the string we are currently processing; in the first case + * that would be: + * mCurToken = script-src + * which allows to do simple string comparisons to see if mCurToken is a valid directive. + * + * Continuing parsing, the parser consumes the next string of that array, resetting: + * mCurToken = "http://www.example.com" + * ^ ^ + * mCurChar mEndChar (points *after* the 'm') + * mCurValue = "" + * + * After calling advance() the first time, helpers would hold the following values: + * mCurToken = "http://www.example.com" + * ^ ^ + * mCurChar mEndChar (points *after* the 'm') + * mCurValue = "h" + * + * We continue parsing till all strings of one directive are consumed, then we reset + * mCurDir to hold the next array of strings and start the process all over. + */ + + const char16_t* mCurChar; + const char16_t* mEndChar; + nsString mCurValue; + nsString mCurToken; + nsTArray<nsString> mCurDir; + + // helpers to allow invalidation of srcs within script-src and style-src + // if either 'strict-dynamic' or at least a hash or nonce is present. + bool mHasHashOrNonce; // false, if no hash or nonce is defined + bool mStrictDynamic; // false, if 'strict-dynamic' is not defined + nsCSPKeywordSrc* mUnsafeInlineKeywordSrc; // null, otherwise invlidate() + + // cache variables for child-src and frame-src directive handling. + // frame-src is deprecated in favor of child-src, however if we + // see a frame-src directive, it takes precedence for frames and iframes. + // At the end of parsing, if we have a child-src directive, we need to + // decide whether it will handle frames, or if there is a frame-src we + // should honor instead. + nsCSPChildSrcDirective* mChildSrc; + nsCSPDirective* mFrameSrc; + + cspTokens mTokens; + nsIURI* mSelfURI; + nsCSPPolicy* mPolicy; + nsCSPContext* mCSPContext; // used for console logging + bool mDeliveredViaMetaTag; +}; + +#endif /* nsCSPParser_h___ */ diff --git a/dom/security/nsCSPService.cpp b/dom/security/nsCSPService.cpp new file mode 100644 index 000000000..7344e19fa --- /dev/null +++ b/dom/security/nsCSPService.cpp @@ -0,0 +1,336 @@ +/* -*- 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 "mozilla/Logging.h" +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsIURI.h" +#include "nsIPrincipal.h" +#include "nsIObserver.h" +#include "nsIContent.h" +#include "nsCSPService.h" +#include "nsIContentSecurityPolicy.h" +#include "nsError.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsAsyncRedirectVerifyHelper.h" +#include "mozilla/Preferences.h" +#include "nsIScriptError.h" +#include "nsContentUtils.h" +#include "nsContentPolicyUtils.h" +#include "nsPrincipal.h" + +using namespace mozilla; + +/* Keeps track of whether or not CSP is enabled */ +bool CSPService::sCSPEnabled = true; + +static LazyLogModule gCspPRLog("CSP"); + +CSPService::CSPService() +{ + Preferences::AddBoolVarCache(&sCSPEnabled, "security.csp.enable"); +} + +CSPService::~CSPService() +{ + mAppStatusCache.Clear(); +} + +NS_IMPL_ISUPPORTS(CSPService, nsIContentPolicy, nsIChannelEventSink) + +// Helper function to identify protocols and content types not subject to CSP. +bool +subjectToCSP(nsIURI* aURI, nsContentPolicyType aContentType) { + // These content types are not subject to CSP content policy checks: + // TYPE_CSP_REPORT -- csp can't block csp reports + // TYPE_REFRESH -- never passed to ShouldLoad (see nsIContentPolicy.idl) + // TYPE_DOCUMENT -- used for frame-ancestors + if (aContentType == nsIContentPolicy::TYPE_CSP_REPORT || + aContentType == nsIContentPolicy::TYPE_REFRESH || + aContentType == nsIContentPolicy::TYPE_DOCUMENT) { + return false; + } + + // The three protocols: data:, blob: and filesystem: share the same + // protocol flag (URI_IS_LOCAL_RESOURCE) with other protocols, like + // chrome:, resource:, moz-icon:, but those three protocols get + // special attention in CSP and are subject to CSP, hence we have + // to make sure those protocols are subject to CSP, see: + // http://www.w3.org/TR/CSP2/#source-list-guid-matching + bool match = false; + nsresult rv = aURI->SchemeIs("data", &match); + if (NS_SUCCEEDED(rv) && match) { + return true; + } + rv = aURI->SchemeIs("blob", &match); + if (NS_SUCCEEDED(rv) && match) { + return true; + } + rv = aURI->SchemeIs("filesystem", &match); + if (NS_SUCCEEDED(rv) && match) { + return true; + } + // finally we have to whitelist "about:" which does not fall in + // any of the two categories underneath but is not subject to CSP. + rv = aURI->SchemeIs("about", &match); + if (NS_SUCCEEDED(rv) && match) { + return false; + } + + // Other protocols are not subject to CSP and can be whitelisted: + // * URI_IS_LOCAL_RESOURCE + // e.g. chrome:, data:, blob:, resource:, moz-icon: + // * URI_INHERITS_SECURITY_CONTEXT + // e.g. javascript: + // + // Please note that it should be possible for websites to + // whitelist their own protocol handlers with respect to CSP, + // hence we use protocol flags to accomplish that. + rv = NS_URIChainHasFlags(aURI, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE, &match); + if (NS_SUCCEEDED(rv) && match) { + return false; + } + rv = NS_URIChainHasFlags(aURI, nsIProtocolHandler::URI_INHERITS_SECURITY_CONTEXT, &match); + if (NS_SUCCEEDED(rv) && match) { + return false; + } + // all other protocols are subject To CSP. + return true; +} + +/* nsIContentPolicy implementation */ +NS_IMETHODIMP +CSPService::ShouldLoad(uint32_t aContentType, + nsIURI *aContentLocation, + nsIURI *aRequestOrigin, + nsISupports *aRequestContext, + const nsACString &aMimeTypeGuess, + nsISupports *aExtra, + nsIPrincipal *aRequestPrincipal, + int16_t *aDecision) +{ + if (!aContentLocation) { + return NS_ERROR_FAILURE; + } + + if (MOZ_LOG_TEST(gCspPRLog, LogLevel::Debug)) { + MOZ_LOG(gCspPRLog, LogLevel::Debug, + ("CSPService::ShouldLoad called for %s", + aContentLocation->GetSpecOrDefault().get())); + } + + // default decision, CSP can revise it if there's a policy to enforce + *aDecision = nsIContentPolicy::ACCEPT; + + // No need to continue processing if CSP is disabled or if the protocol + // or type is *not* subject to CSP. + // Please note, the correct way to opt-out of CSP using a custom + // protocolHandler is to set one of the nsIProtocolHandler flags + // that are whitelistet in subjectToCSP() + if (!sCSPEnabled || !subjectToCSP(aContentLocation, aContentType)) { + return NS_OK; + } + + // query the principal of the document; if no document is passed, then + // fall back to using the requestPrincipal (e.g. service workers do not + // pass a document). + nsCOMPtr<nsINode> node(do_QueryInterface(aRequestContext)); + nsCOMPtr<nsIPrincipal> principal = node ? node->NodePrincipal() + : aRequestPrincipal; + if (!principal) { + // if we can't query a principal, then there is nothing to do. + return NS_OK; + } + nsresult rv = NS_OK; + + // 1) Apply speculate CSP for preloads + bool isPreload = nsContentUtils::IsPreloadType(aContentType); + + if (isPreload) { + nsCOMPtr<nsIContentSecurityPolicy> preloadCsp; + rv = principal->GetPreloadCsp(getter_AddRefs(preloadCsp)); + NS_ENSURE_SUCCESS(rv, rv); + + if (preloadCsp) { + // obtain the enforcement decision + // (don't pass aExtra, we use that slot for redirects) + rv = preloadCsp->ShouldLoad(aContentType, + aContentLocation, + aRequestOrigin, + aRequestContext, + aMimeTypeGuess, + nullptr, // aExtra + aDecision); + NS_ENSURE_SUCCESS(rv, rv); + + // if the preload policy already denied the load, then there + // is no point in checking the real policy + if (NS_CP_REJECTED(*aDecision)) { + return NS_OK; + } + } + } + + // 2) Apply actual CSP to all loads + nsCOMPtr<nsIContentSecurityPolicy> csp; + rv = principal->GetCsp(getter_AddRefs(csp)); + NS_ENSURE_SUCCESS(rv, rv); + + if (csp) { + // obtain the enforcement decision + // (don't pass aExtra, we use that slot for redirects) + rv = csp->ShouldLoad(aContentType, + aContentLocation, + aRequestOrigin, + aRequestContext, + aMimeTypeGuess, + nullptr, + aDecision); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +NS_IMETHODIMP +CSPService::ShouldProcess(uint32_t aContentType, + nsIURI *aContentLocation, + nsIURI *aRequestOrigin, + nsISupports *aRequestContext, + const nsACString &aMimeTypeGuess, + nsISupports *aExtra, + nsIPrincipal *aRequestPrincipal, + int16_t *aDecision) +{ + if (!aContentLocation) { + return NS_ERROR_FAILURE; + } + + if (MOZ_LOG_TEST(gCspPRLog, LogLevel::Debug)) { + MOZ_LOG(gCspPRLog, LogLevel::Debug, + ("CSPService::ShouldProcess called for %s", + aContentLocation->GetSpecOrDefault().get())); + } + + // ShouldProcess is only relevant to TYPE_OBJECT, so let's convert the + // internal contentPolicyType to the mapping external one. + // If it is not TYPE_OBJECT, we can return at this point. + // Note that we should still pass the internal contentPolicyType + // (aContentType) to ShouldLoad(). + uint32_t policyType = + nsContentUtils::InternalContentPolicyTypeToExternal(aContentType); + + if (policyType != nsIContentPolicy::TYPE_OBJECT) { + *aDecision = nsIContentPolicy::ACCEPT; + return NS_OK; + } + + return ShouldLoad(aContentType, + aContentLocation, + aRequestOrigin, + aRequestContext, + aMimeTypeGuess, + aExtra, + aRequestPrincipal, + aDecision); +} + +/* nsIChannelEventSink implementation */ +NS_IMETHODIMP +CSPService::AsyncOnChannelRedirect(nsIChannel *oldChannel, + nsIChannel *newChannel, + uint32_t flags, + nsIAsyncVerifyRedirectCallback *callback) +{ + net::nsAsyncRedirectAutoCallback autoCallback(callback); + + nsCOMPtr<nsIURI> newUri; + nsresult rv = newChannel->GetURI(getter_AddRefs(newUri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILoadInfo> loadInfo = oldChannel->GetLoadInfo(); + + // if no loadInfo on the channel, nothing for us to do + if (!loadInfo) { + return NS_OK; + } + + // No need to continue processing if CSP is disabled or if the protocol + // is *not* subject to CSP. + // Please note, the correct way to opt-out of CSP using a custom + // protocolHandler is to set one of the nsIProtocolHandler flags + // that are whitelistet in subjectToCSP() + nsContentPolicyType policyType = loadInfo->InternalContentPolicyType(); + if (!sCSPEnabled || !subjectToCSP(newUri, policyType)) { + return NS_OK; + } + + /* Since redirecting channels don't call into nsIContentPolicy, we call our + * Content Policy implementation directly when redirects occur using the + * information set in the LoadInfo when channels are created. + * + * We check if the CSP permits this host for this type of load, if not, + * we cancel the load now. + */ + nsCOMPtr<nsIURI> originalUri; + rv = oldChannel->GetOriginalURI(getter_AddRefs(originalUri)); + NS_ENSURE_SUCCESS(rv, rv); + + bool isPreload = nsContentUtils::IsPreloadType(policyType); + + /* On redirect, if the content policy is a preload type, rejecting the preload + * results in the load silently failing, so we convert preloads to the actual + * type. See Bug 1219453. + */ + policyType = + nsContentUtils::InternalContentPolicyTypeToExternalOrWorker(policyType); + + int16_t aDecision = nsIContentPolicy::ACCEPT; + // 1) Apply speculative CSP for preloads + if (isPreload) { + nsCOMPtr<nsIContentSecurityPolicy> preloadCsp; + loadInfo->LoadingPrincipal()->GetPreloadCsp(getter_AddRefs(preloadCsp)); + + if (preloadCsp) { + // Pass originalURI as aExtra to indicate the redirect + preloadCsp->ShouldLoad(policyType, // load type per nsIContentPolicy (uint32_t) + newUri, // nsIURI + nullptr, // nsIURI + nullptr, // nsISupports + EmptyCString(), // ACString - MIME guess + originalUri, // aExtra + &aDecision); + + // if the preload policy already denied the load, then there + // is no point in checking the real policy + if (NS_CP_REJECTED(aDecision)) { + autoCallback.DontCallback(); + return NS_BINDING_FAILED; + } + } + } + + // 2) Apply actual CSP to all loads + nsCOMPtr<nsIContentSecurityPolicy> csp; + loadInfo->LoadingPrincipal()->GetCsp(getter_AddRefs(csp)); + + if (csp) { + // Pass originalURI as aExtra to indicate the redirect + csp->ShouldLoad(policyType, // load type per nsIContentPolicy (uint32_t) + newUri, // nsIURI + nullptr, // nsIURI + nullptr, // nsISupports + EmptyCString(), // ACString - MIME guess + originalUri, // aExtra + &aDecision); + } + + // if ShouldLoad doesn't accept the load, cancel the request + if (!NS_CP_ACCEPTED(aDecision)) { + autoCallback.DontCallback(); + return NS_BINDING_FAILED; + } + return NS_OK; +} diff --git a/dom/security/nsCSPService.h b/dom/security/nsCSPService.h new file mode 100644 index 000000000..0eb991233 --- /dev/null +++ b/dom/security/nsCSPService.h @@ -0,0 +1,38 @@ +/* -*- 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 nsCSPService_h___ +#define nsCSPService_h___ + +#include "nsXPCOM.h" +#include "nsIContentPolicy.h" +#include "nsIChannel.h" +#include "nsIChannelEventSink.h" +#include "nsDataHashtable.h" + +#define CSPSERVICE_CONTRACTID "@mozilla.org/cspservice;1" +#define CSPSERVICE_CID \ + { 0x8d2f40b2, 0x4875, 0x4c95, \ + { 0x97, 0xd9, 0x3f, 0x7d, 0xca, 0x2c, 0xb4, 0x60 } } +class CSPService : public nsIContentPolicy, + public nsIChannelEventSink +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTPOLICY + NS_DECL_NSICHANNELEVENTSINK + + CSPService(); + static bool sCSPEnabled; + +protected: + virtual ~CSPService(); + +private: + // Maps origins to app status. + nsDataHashtable<nsCStringHashKey, uint16_t> mAppStatusCache; +}; +#endif /* nsCSPService_h___ */ diff --git a/dom/security/nsCSPUtils.cpp b/dom/security/nsCSPUtils.cpp new file mode 100644 index 000000000..63b4aae2c --- /dev/null +++ b/dom/security/nsCSPUtils.cpp @@ -0,0 +1,1616 @@ +/* -*- 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 "nsAttrValue.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsContentUtils.h" +#include "nsCSPUtils.h" +#include "nsDebug.h" +#include "nsIConsoleService.h" +#include "nsICryptoHash.h" +#include "nsIScriptError.h" +#include "nsIServiceManager.h" +#include "nsIStringBundle.h" +#include "nsIURL.h" +#include "nsReadableUtils.h" +#include "nsSandboxFlags.h" + +#define DEFAULT_PORT -1 + +static mozilla::LogModule* +GetCspUtilsLog() +{ + static mozilla::LazyLogModule gCspUtilsPRLog("CSPUtils"); + return gCspUtilsPRLog; +} + +#define CSPUTILSLOG(args) MOZ_LOG(GetCspUtilsLog(), mozilla::LogLevel::Debug, args) +#define CSPUTILSLOGENABLED() MOZ_LOG_TEST(GetCspUtilsLog(), mozilla::LogLevel::Debug) + +void +CSP_PercentDecodeStr(const nsAString& aEncStr, nsAString& outDecStr) +{ + outDecStr.Truncate(); + + // helper function that should not be visible outside this methods scope + struct local { + static inline char16_t convertHexDig(char16_t aHexDig) { + if (isNumberToken(aHexDig)) { + return aHexDig - '0'; + } + if (aHexDig >= 'A' && aHexDig <= 'F') { + return aHexDig - 'A' + 10; + } + // must be a lower case character + // (aHexDig >= 'a' && aHexDig <= 'f') + return aHexDig - 'a' + 10; + } + }; + + const char16_t *cur, *end, *hexDig1, *hexDig2; + cur = aEncStr.BeginReading(); + end = aEncStr.EndReading(); + + while (cur != end) { + // if it's not a percent sign then there is + // nothing to do for that character + if (*cur != PERCENT_SIGN) { + outDecStr.Append(*cur); + cur++; + continue; + } + + // get the two hexDigs following the '%'-sign + hexDig1 = cur + 1; + hexDig2 = cur + 2; + + // if there are no hexdigs after the '%' then + // there is nothing to do for us. + if (hexDig1 == end || hexDig2 == end || + !isValidHexDig(*hexDig1) || + !isValidHexDig(*hexDig2)) { + outDecStr.Append(PERCENT_SIGN); + cur++; + continue; + } + + // decode "% hexDig1 hexDig2" into a character. + char16_t decChar = (local::convertHexDig(*hexDig1) << 4) + + local::convertHexDig(*hexDig2); + outDecStr.Append(decChar); + + // increment 'cur' to after the second hexDig + cur = ++hexDig2; + } +} + +void +CSP_GetLocalizedStr(const char16_t* aName, + const char16_t** aParams, + uint32_t aLength, + char16_t** outResult) +{ + nsCOMPtr<nsIStringBundle> keyStringBundle; + nsCOMPtr<nsIStringBundleService> stringBundleService = + mozilla::services::GetStringBundleService(); + + NS_ASSERTION(stringBundleService, "String bundle service must be present!"); + stringBundleService->CreateBundle("chrome://global/locale/security/csp.properties", + getter_AddRefs(keyStringBundle)); + + NS_ASSERTION(keyStringBundle, "Key string bundle must be available!"); + + if (!keyStringBundle) { + return; + } + keyStringBundle->FormatStringFromName(aName, aParams, aLength, outResult); +} + +void +CSP_LogStrMessage(const nsAString& aMsg) +{ + nsCOMPtr<nsIConsoleService> console(do_GetService("@mozilla.org/consoleservice;1")); + + if (!console) { + return; + } + nsString msg = PromiseFlatString(aMsg); + console->LogStringMessage(msg.get()); +} + +void +CSP_LogMessage(const nsAString& aMessage, + const nsAString& aSourceName, + const nsAString& aSourceLine, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aFlags, + const char *aCategory, + uint64_t aInnerWindowID) +{ + nsCOMPtr<nsIConsoleService> console(do_GetService(NS_CONSOLESERVICE_CONTRACTID)); + + nsCOMPtr<nsIScriptError> error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID)); + + if (!console || !error) { + return; + } + + // Prepending CSP to the outgoing console message + nsString cspMsg; + cspMsg.Append(NS_LITERAL_STRING("Content Security Policy: ")); + cspMsg.Append(aMessage); + + // Currently 'aSourceLine' is not logged to the console, because similar + // information is already included within the source link of the message. + // For inline violations however, the line and column number are 0 and + // information contained within 'aSourceLine' can be really useful for devs. + // E.g. 'aSourceLine' might be: 'onclick attribute on DIV element'. + // In such cases we append 'aSourceLine' directly to the error message. + if (!aSourceLine.IsEmpty()) { + cspMsg.Append(NS_LITERAL_STRING(" Source: ")); + cspMsg.Append(aSourceLine); + cspMsg.Append(NS_LITERAL_STRING(".")); + } + + nsresult rv; + if (aInnerWindowID > 0) { + nsCString catStr; + catStr.AssignASCII(aCategory); + rv = error->InitWithWindowID(cspMsg, aSourceName, + aSourceLine, aLineNumber, + aColumnNumber, aFlags, + catStr, aInnerWindowID); + } + else { + rv = error->Init(cspMsg, aSourceName, + aSourceLine, aLineNumber, + aColumnNumber, aFlags, + aCategory); + } + if (NS_FAILED(rv)) { + return; + } + console->LogMessage(error); +} + +/** + * Combines CSP_LogMessage and CSP_GetLocalizedStr into one call. + */ +void +CSP_LogLocalizedStr(const char16_t* aName, + const char16_t** aParams, + uint32_t aLength, + const nsAString& aSourceName, + const nsAString& aSourceLine, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aFlags, + const char* aCategory, + uint64_t aInnerWindowID) +{ + nsXPIDLString logMsg; + CSP_GetLocalizedStr(aName, aParams, aLength, getter_Copies(logMsg)); + CSP_LogMessage(logMsg, aSourceName, aSourceLine, + aLineNumber, aColumnNumber, aFlags, + aCategory, aInnerWindowID); +} + +/* ===== Helpers ============================ */ +CSPDirective +CSP_ContentTypeToDirective(nsContentPolicyType aType) +{ + switch (aType) { + case nsIContentPolicy::TYPE_IMAGE: + case nsIContentPolicy::TYPE_IMAGESET: + return nsIContentSecurityPolicy::IMG_SRC_DIRECTIVE; + + // BLock XSLT as script, see bug 910139 + case nsIContentPolicy::TYPE_XSLT: + case nsIContentPolicy::TYPE_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD: + return nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_STYLESHEET: + return nsIContentSecurityPolicy::STYLE_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_FONT: + return nsIContentSecurityPolicy::FONT_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_MEDIA: + return nsIContentSecurityPolicy::MEDIA_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_WEB_MANIFEST: + return nsIContentSecurityPolicy::WEB_MANIFEST_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_INTERNAL_WORKER: + case nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER: + case nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER: + return nsIContentSecurityPolicy::CHILD_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_SUBDOCUMENT: + return nsIContentSecurityPolicy::FRAME_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_WEBSOCKET: + case nsIContentPolicy::TYPE_XMLHTTPREQUEST: + case nsIContentPolicy::TYPE_BEACON: + case nsIContentPolicy::TYPE_PING: + case nsIContentPolicy::TYPE_FETCH: + return nsIContentSecurityPolicy::CONNECT_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_OBJECT: + case nsIContentPolicy::TYPE_OBJECT_SUBREQUEST: + return nsIContentSecurityPolicy::OBJECT_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_XBL: + case nsIContentPolicy::TYPE_DTD: + case nsIContentPolicy::TYPE_OTHER: + return nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE; + + // csp shold not block top level loads, e.g. in case + // of a redirect. + case nsIContentPolicy::TYPE_DOCUMENT: + // CSP can not block csp reports + case nsIContentPolicy::TYPE_CSP_REPORT: + return nsIContentSecurityPolicy::NO_DIRECTIVE; + + // Fall through to error for all other directives + default: + MOZ_ASSERT(false, "Can not map nsContentPolicyType to CSPDirective"); + } + return nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE; +} + +nsCSPHostSrc* +CSP_CreateHostSrcFromURI(nsIURI* aURI) +{ + // Create the host first + nsCString host; + aURI->GetHost(host); + nsCSPHostSrc *hostsrc = new nsCSPHostSrc(NS_ConvertUTF8toUTF16(host)); + + // Add the scheme. + nsCString scheme; + aURI->GetScheme(scheme); + hostsrc->setScheme(NS_ConvertUTF8toUTF16(scheme)); + + int32_t port; + aURI->GetPort(&port); + // Only add port if it's not default port. + if (port > 0) { + nsAutoString portStr; + portStr.AppendInt(port); + hostsrc->setPort(portStr); + } + return hostsrc; +} + +bool +CSP_IsValidDirective(const nsAString& aDir) +{ + uint32_t numDirs = (sizeof(CSPStrDirectives) / sizeof(CSPStrDirectives[0])); + + for (uint32_t i = 0; i < numDirs; i++) { + if (aDir.LowerCaseEqualsASCII(CSPStrDirectives[i])) { + return true; + } + } + return false; +} +bool +CSP_IsDirective(const nsAString& aValue, CSPDirective aDir) +{ + return aValue.LowerCaseEqualsASCII(CSP_CSPDirectiveToString(aDir)); +} + +bool +CSP_IsKeyword(const nsAString& aValue, enum CSPKeyword aKey) +{ + return aValue.LowerCaseEqualsASCII(CSP_EnumToKeyword(aKey)); +} + +bool +CSP_IsQuotelessKeyword(const nsAString& aKey) +{ + nsString lowerKey = PromiseFlatString(aKey); + ToLowerCase(lowerKey); + + static_assert(CSP_LAST_KEYWORD_VALUE == + (sizeof(CSPStrKeywords) / sizeof(CSPStrKeywords[0])), + "CSP_LAST_KEYWORD_VALUE does not match length of CSPStrKeywords"); + + nsAutoString keyword; + for (uint32_t i = 0; i < CSP_LAST_KEYWORD_VALUE; i++) { + // skipping the leading ' and trimming the trailing ' + keyword.AssignASCII(CSPStrKeywords[i] + 1); + keyword.Trim("'", false, true); + if (lowerKey.Equals(keyword)) { + return true; + } + } + return false; +} + +/* + * Checks whether the current directive permits a specific + * scheme. This function is called from nsCSPSchemeSrc() and + * also nsCSPHostSrc. + * @param aEnforcementScheme + * The scheme that this directive allows + * @param aUri + * The uri of the subresource load. + * @param aReportOnly + * Whether the enforced policy is report only or not. + * @param aUpgradeInsecure + * Whether the policy makes use of the directive + * 'upgrade-insecure-requests'. + */ + +bool +permitsScheme(const nsAString& aEnforcementScheme, + nsIURI* aUri, + bool aReportOnly, + bool aUpgradeInsecure) +{ + nsAutoCString scheme; + nsresult rv = aUri->GetScheme(scheme); + NS_ENSURE_SUCCESS(rv, false); + + // no scheme to enforce, let's allow the load (e.g. script-src *) + if (aEnforcementScheme.IsEmpty()) { + return true; + } + + // if the scheme matches, all good - allow the load + if (aEnforcementScheme.EqualsASCII(scheme.get())) { + return true; + } + + // allow scheme-less sources where the protected resource is http + // and the load is https, see: + // http://www.w3.org/TR/CSP2/#match-source-expression + if (aEnforcementScheme.EqualsASCII("http") && + scheme.EqualsASCII("https")) { + return true; + } + + // Allow the load when enforcing upgrade-insecure-requests with the + // promise the request gets upgraded from http to https and ws to wss. + // See nsHttpChannel::Connect() and also WebSocket.cpp. Please note, + // the report only policies should not allow the load and report + // the error back to the page. + return ((aUpgradeInsecure && !aReportOnly) && + ((scheme.EqualsASCII("http") && aEnforcementScheme.EqualsASCII("https")) || + (scheme.EqualsASCII("ws") && aEnforcementScheme.EqualsASCII("wss")))); +} + +/* + * A helper function for appending a CSP header to an existing CSP + * policy. + * + * @param aCsp the CSP policy + * @param aHeaderValue the header + * @param aReportOnly is this a report-only header? + */ + +nsresult +CSP_AppendCSPFromHeader(nsIContentSecurityPolicy* aCsp, + const nsAString& aHeaderValue, + bool aReportOnly) +{ + NS_ENSURE_ARG(aCsp); + + // Need to tokenize the header value since multiple headers could be + // concatenated into one comma-separated list of policies. + // See RFC2616 section 4.2 (last paragraph) + nsresult rv = NS_OK; + nsCharSeparatedTokenizer tokenizer(aHeaderValue, ','); + while (tokenizer.hasMoreTokens()) { + const nsSubstring& policy = tokenizer.nextToken(); + rv = aCsp->AppendPolicy(policy, aReportOnly, false); + NS_ENSURE_SUCCESS(rv, rv); + { + CSPUTILSLOG(("CSP refined with policy: \"%s\"", + NS_ConvertUTF16toUTF8(policy).get())); + } + } + return NS_OK; +} + +/* ===== nsCSPSrc ============================ */ + +nsCSPBaseSrc::nsCSPBaseSrc() + : mInvalidated(false) +{ +} + +nsCSPBaseSrc::~nsCSPBaseSrc() +{ +} + +// ::permits is only called for external load requests, therefore: +// nsCSPKeywordSrc and nsCSPHashSource fall back to this base class +// implementation which will never allow the load. +bool +nsCSPBaseSrc::permits(nsIURI* aUri, const nsAString& aNonce, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure, bool aParserCreated) const +{ + if (CSPUTILSLOGENABLED()) { + CSPUTILSLOG(("nsCSPBaseSrc::permits, aUri: %s", + aUri->GetSpecOrDefault().get())); + } + return false; +} + +// ::allows is only called for inlined loads, therefore: +// nsCSPSchemeSrc, nsCSPHostSrc fall back +// to this base class implementation which will never allow the load. +bool +nsCSPBaseSrc::allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const +{ + CSPUTILSLOG(("nsCSPBaseSrc::allows, aKeyWord: %s, a HashOrNonce: %s", + aKeyword == CSP_HASH ? "hash" : CSP_EnumToKeyword(aKeyword), + NS_ConvertUTF16toUTF8(aHashOrNonce).get())); + return false; +} + +/* ====== nsCSPSchemeSrc ===================== */ + +nsCSPSchemeSrc::nsCSPSchemeSrc(const nsAString& aScheme) + : mScheme(aScheme) +{ + ToLowerCase(mScheme); +} + +nsCSPSchemeSrc::~nsCSPSchemeSrc() +{ +} + +bool +nsCSPSchemeSrc::permits(nsIURI* aUri, const nsAString& aNonce, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure, bool aParserCreated) const +{ + if (CSPUTILSLOGENABLED()) { + CSPUTILSLOG(("nsCSPSchemeSrc::permits, aUri: %s", + aUri->GetSpecOrDefault().get())); + } + MOZ_ASSERT((!mScheme.EqualsASCII("")), "scheme can not be the empty string"); + if (mInvalidated) { + return false; + } + return permitsScheme(mScheme, aUri, aReportOnly, aUpgradeInsecure); +} + +bool +nsCSPSchemeSrc::visit(nsCSPSrcVisitor* aVisitor) const +{ + return aVisitor->visitSchemeSrc(*this); +} + +void +nsCSPSchemeSrc::toString(nsAString& outStr) const +{ + outStr.Append(mScheme); + outStr.AppendASCII(":"); +} + +/* ===== nsCSPHostSrc ======================== */ + +nsCSPHostSrc::nsCSPHostSrc(const nsAString& aHost) + : mHost(aHost) +{ + ToLowerCase(mHost); +} + +nsCSPHostSrc::~nsCSPHostSrc() +{ +} + +/* + * Checks whether the current directive permits a specific port. + * @param aEnforcementScheme + * The scheme that this directive allows + * (used to query the default port for that scheme) + * @param aEnforcementPort + * The port that this directive allows + * @param aResourceURI + * The uri of the subresource load + */ +bool +permitsPort(const nsAString& aEnforcementScheme, + const nsAString& aEnforcementPort, + nsIURI* aResourceURI) +{ + // If enforcement port is the wildcard, don't block the load. + if (aEnforcementPort.EqualsASCII("*")) { + return true; + } + + int32_t resourcePort; + nsresult rv = aResourceURI->GetPort(&resourcePort); + NS_ENSURE_SUCCESS(rv, false); + + // Avoid unnecessary string creation/manipulation and don't block the + // load if the resource to be loaded uses the default port for that + // scheme and there is no port to be enforced. + // Note, this optimization relies on scheme checks within permitsScheme(). + if (resourcePort == DEFAULT_PORT && aEnforcementPort.IsEmpty()) { + return true; + } + + // By now we know at that either the resourcePort does not use the default + // port or there is a port restriction to be enforced. A port value of -1 + // corresponds to the protocol's default port (eg. -1 implies port 80 for + // http URIs), in such a case we have to query the default port of the + // resource to be loaded. + if (resourcePort == DEFAULT_PORT) { + nsAutoCString resourceScheme; + rv = aResourceURI->GetScheme(resourceScheme); + NS_ENSURE_SUCCESS(rv, false); + resourcePort = NS_GetDefaultPort(resourceScheme.get()); + } + + // If there is a port to be enforced and the ports match, then + // don't block the load. + nsString resourcePortStr; + resourcePortStr.AppendInt(resourcePort); + if (aEnforcementPort.Equals(resourcePortStr)) { + return true; + } + + // If there is no port to be enforced, query the default port for the load. + nsString enforcementPort(aEnforcementPort); + if (enforcementPort.IsEmpty()) { + // For scheme less sources, our parser always generates a scheme + // which is the scheme of the protected resource. + MOZ_ASSERT(!aEnforcementScheme.IsEmpty(), + "need a scheme to query default port"); + int32_t defaultEnforcementPort = + NS_GetDefaultPort(NS_ConvertUTF16toUTF8(aEnforcementScheme).get()); + enforcementPort.Truncate(); + enforcementPort.AppendInt(defaultEnforcementPort); + } + + // If default ports match, don't block the load + if (enforcementPort.Equals(resourcePortStr)) { + return true; + } + + // Additional port matching where the regular URL matching algorithm + // treats insecure ports as matching their secure variants. + // default port for http is :80 + // default port for https is :443 + if (enforcementPort.EqualsLiteral("80") && + resourcePortStr.EqualsLiteral("443")) { + return true; + } + + // ports do not match, block the load. + return false; +} + +bool +nsCSPHostSrc::permits(nsIURI* aUri, const nsAString& aNonce, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure, bool aParserCreated) const +{ + if (CSPUTILSLOGENABLED()) { + CSPUTILSLOG(("nsCSPHostSrc::permits, aUri: %s", + aUri->GetSpecOrDefault().get())); + } + + if (mInvalidated) { + return false; + } + + // we are following the enforcement rules from the spec, see: + // http://www.w3.org/TR/CSP11/#match-source-expression + + // 4.3) scheme matching: Check if the scheme matches. + if (!permitsScheme(mScheme, aUri, aReportOnly, aUpgradeInsecure)) { + return false; + } + + // The host in nsCSpHostSrc should never be empty. In case we are enforcing + // just a specific scheme, the parser should generate a nsCSPSchemeSource. + NS_ASSERTION((!mHost.IsEmpty()), "host can not be the empty string"); + + // 2) host matching: Enforce a single * + if (mHost.EqualsASCII("*")) { + // The single ASTERISK character (*) does not match a URI's scheme of a type + // designating a globally unique identifier (such as blob:, data:, or filesystem:) + // At the moment firefox does not support filesystem; but for future compatibility + // we support it in CSP according to the spec, see: 4.2.2 Matching Source Expressions + // Note, that whitelisting any of these schemes would call nsCSPSchemeSrc::permits(). + bool isBlobScheme = + (NS_SUCCEEDED(aUri->SchemeIs("blob", &isBlobScheme)) && isBlobScheme); + bool isDataScheme = + (NS_SUCCEEDED(aUri->SchemeIs("data", &isDataScheme)) && isDataScheme); + bool isFileScheme = + (NS_SUCCEEDED(aUri->SchemeIs("filesystem", &isFileScheme)) && isFileScheme); + + if (isBlobScheme || isDataScheme || isFileScheme) { + return false; + } + return true; + } + + // Before we can check if the host matches, we have to + // extract the host part from aUri. + nsAutoCString uriHost; + nsresult rv = aUri->GetHost(uriHost); + NS_ENSURE_SUCCESS(rv, false); + + nsString decodedUriHost; + CSP_PercentDecodeStr(NS_ConvertUTF8toUTF16(uriHost), decodedUriHost); + + // 4.5) host matching: Check if the allowed host starts with a wilcard. + if (mHost.First() == '*') { + NS_ASSERTION(mHost[1] == '.', "Second character needs to be '.' whenever host starts with '*'"); + + // Eliminate leading "*", but keeping the FULL STOP (.) thereafter before checking + // if the remaining characters match + nsString wildCardHost = mHost; + wildCardHost = Substring(wildCardHost, 1, wildCardHost.Length() - 1); + if (!StringEndsWith(decodedUriHost, wildCardHost)) { + return false; + } + } + // 4.6) host matching: Check if hosts match. + else if (!mHost.Equals(decodedUriHost)) { + return false; + } + + // Port matching: Check if the ports match. + if (!permitsPort(mScheme, mPort, aUri)) { + return false; + } + + // 4.9) Path matching: If there is a path, we have to enforce + // path-level matching, unless the channel got redirected, see: + // http://www.w3.org/TR/CSP11/#source-list-paths-and-redirects + if (!aWasRedirected && !mPath.IsEmpty()) { + // converting aUri into nsIURL so we can strip query and ref + // example.com/test#foo -> example.com/test + // example.com/test?val=foo -> example.com/test + nsCOMPtr<nsIURL> url = do_QueryInterface(aUri); + if (!url) { + NS_ASSERTION(false, "can't QI into nsIURI"); + return false; + } + nsAutoCString uriPath; + rv = url->GetFilePath(uriPath); + NS_ENSURE_SUCCESS(rv, false); + + nsString decodedUriPath; + CSP_PercentDecodeStr(NS_ConvertUTF8toUTF16(uriPath), decodedUriPath); + + // check if the last character of mPath is '/'; if so + // we just have to check loading resource is within + // the allowed path. + if (mPath.Last() == '/') { + if (!StringBeginsWith(decodedUriPath, mPath)) { + return false; + } + } + // otherwise mPath whitelists a specific file, and we have to + // check if the loading resource matches that whitelisted file. + else { + if (!mPath.Equals(decodedUriPath)) { + return false; + } + } + } + + // At the end: scheme, host, port and path match -> allow the load. + return true; +} + +bool +nsCSPHostSrc::visit(nsCSPSrcVisitor* aVisitor) const +{ + return aVisitor->visitHostSrc(*this); +} + +void +nsCSPHostSrc::toString(nsAString& outStr) const +{ + // If mHost is a single "*", we append the wildcard and return. + if (mHost.EqualsASCII("*") && + mScheme.IsEmpty() && + mPort.IsEmpty()) { + outStr.Append(mHost); + return; + } + + // append scheme + outStr.Append(mScheme); + + // append host + outStr.AppendASCII("://"); + outStr.Append(mHost); + + // append port + if (!mPort.IsEmpty()) { + outStr.AppendASCII(":"); + outStr.Append(mPort); + } + + // append path + outStr.Append(mPath); +} + +void +nsCSPHostSrc::setScheme(const nsAString& aScheme) +{ + mScheme = aScheme; + ToLowerCase(mScheme); +} + +void +nsCSPHostSrc::setPort(const nsAString& aPort) +{ + mPort = aPort; +} + +void +nsCSPHostSrc::appendPath(const nsAString& aPath) +{ + mPath.Append(aPath); +} + +/* ===== nsCSPKeywordSrc ===================== */ + +nsCSPKeywordSrc::nsCSPKeywordSrc(enum CSPKeyword aKeyword) + : mKeyword(aKeyword) +{ + NS_ASSERTION((aKeyword != CSP_SELF), + "'self' should have been replaced in the parser"); +} + +nsCSPKeywordSrc::~nsCSPKeywordSrc() +{ +} + +bool +nsCSPKeywordSrc::permits(nsIURI* aUri, const nsAString& aNonce, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure, bool aParserCreated) const +{ + // no need to check for invalidated, this will always return false unless + // it is an nsCSPKeywordSrc for 'strict-dynamic', which should allow non + // parser created scripts. + return ((mKeyword == CSP_STRICT_DYNAMIC) && !aParserCreated); +} + +bool +nsCSPKeywordSrc::allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const +{ + CSPUTILSLOG(("nsCSPKeywordSrc::allows, aKeyWord: %s, aHashOrNonce: %s, mInvalidated: %s", + CSP_EnumToKeyword(aKeyword), + CSP_EnumToKeyword(mKeyword), + NS_ConvertUTF16toUTF8(aHashOrNonce).get(), + mInvalidated ? "yes" : "false")); + + if (mInvalidated) { + // only 'self' and 'unsafe-inline' are keywords that can be ignored. Please note that + // the parser already translates 'self' into a uri (see assertion in constructor). + MOZ_ASSERT(mKeyword == CSP_UNSAFE_INLINE, + "should only invalidate unsafe-inline"); + return false; + } + // either the keyword allows the load or the policy contains 'strict-dynamic', in which + // case we have to make sure the script is not parser created before allowing the load. + return ((mKeyword == aKeyword) || + ((mKeyword == CSP_STRICT_DYNAMIC) && !aParserCreated)); +} + +bool +nsCSPKeywordSrc::visit(nsCSPSrcVisitor* aVisitor) const +{ + return aVisitor->visitKeywordSrc(*this); +} + +void +nsCSPKeywordSrc::toString(nsAString& outStr) const +{ + outStr.AppendASCII(CSP_EnumToKeyword(mKeyword)); +} + +/* ===== nsCSPNonceSrc ==================== */ + +nsCSPNonceSrc::nsCSPNonceSrc(const nsAString& aNonce) + : mNonce(aNonce) +{ +} + +nsCSPNonceSrc::~nsCSPNonceSrc() +{ +} + +bool +nsCSPNonceSrc::permits(nsIURI* aUri, const nsAString& aNonce, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure, bool aParserCreated) const +{ + if (CSPUTILSLOGENABLED()) { + CSPUTILSLOG(("nsCSPNonceSrc::permits, aUri: %s, aNonce: %s", + aUri->GetSpecOrDefault().get(), + NS_ConvertUTF16toUTF8(aNonce).get())); + } + + // nonces can not be invalidated by strict-dynamic + return mNonce.Equals(aNonce); +} + +bool +nsCSPNonceSrc::allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const +{ + CSPUTILSLOG(("nsCSPNonceSrc::allows, aKeyWord: %s, a HashOrNonce: %s", + CSP_EnumToKeyword(aKeyword), NS_ConvertUTF16toUTF8(aHashOrNonce).get())); + + if (aKeyword != CSP_NONCE) { + return false; + } + // nonces can not be invalidated by strict-dynamic + return mNonce.Equals(aHashOrNonce); +} + +bool +nsCSPNonceSrc::visit(nsCSPSrcVisitor* aVisitor) const +{ + return aVisitor->visitNonceSrc(*this); +} + +void +nsCSPNonceSrc::toString(nsAString& outStr) const +{ + outStr.AppendASCII(CSP_EnumToKeyword(CSP_NONCE)); + outStr.Append(mNonce); + outStr.AppendASCII("'"); +} + +/* ===== nsCSPHashSrc ===================== */ + +nsCSPHashSrc::nsCSPHashSrc(const nsAString& aAlgo, const nsAString& aHash) + : mAlgorithm(aAlgo) + , mHash(aHash) +{ + // Only the algo should be rewritten to lowercase, the hash must remain the same. + ToLowerCase(mAlgorithm); +} + +nsCSPHashSrc::~nsCSPHashSrc() +{ +} + +bool +nsCSPHashSrc::allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const +{ + CSPUTILSLOG(("nsCSPHashSrc::allows, aKeyWord: %s, a HashOrNonce: %s", + CSP_EnumToKeyword(aKeyword), NS_ConvertUTF16toUTF8(aHashOrNonce).get())); + + if (aKeyword != CSP_HASH) { + return false; + } + + // hashes can not be invalidated by strict-dynamic + + // Convert aHashOrNonce to UTF-8 + NS_ConvertUTF16toUTF8 utf8_hash(aHashOrNonce); + + nsresult rv; + nsCOMPtr<nsICryptoHash> hasher; + hasher = do_CreateInstance("@mozilla.org/security/hash;1", &rv); + NS_ENSURE_SUCCESS(rv, false); + + rv = hasher->InitWithString(NS_ConvertUTF16toUTF8(mAlgorithm)); + NS_ENSURE_SUCCESS(rv, false); + + rv = hasher->Update((uint8_t *)utf8_hash.get(), utf8_hash.Length()); + NS_ENSURE_SUCCESS(rv, false); + + nsAutoCString hash; + rv = hasher->Finish(true, hash); + NS_ENSURE_SUCCESS(rv, false); + + // The NSS Base64 encoder automatically adds linebreaks "\r\n" every 64 + // characters. We need to remove these so we can properly validate longer + // (SHA-512) base64-encoded hashes + hash.StripChars("\r\n"); + return NS_ConvertUTF16toUTF8(mHash).Equals(hash); +} + +bool +nsCSPHashSrc::visit(nsCSPSrcVisitor* aVisitor) const +{ + return aVisitor->visitHashSrc(*this); +} + +void +nsCSPHashSrc::toString(nsAString& outStr) const +{ + outStr.AppendASCII("'"); + outStr.Append(mAlgorithm); + outStr.AppendASCII("-"); + outStr.Append(mHash); + outStr.AppendASCII("'"); +} + +/* ===== nsCSPReportURI ===================== */ + +nsCSPReportURI::nsCSPReportURI(nsIURI *aURI) + :mReportURI(aURI) +{ +} + +nsCSPReportURI::~nsCSPReportURI() +{ +} + +bool +nsCSPReportURI::visit(nsCSPSrcVisitor* aVisitor) const +{ + return false; +} + +void +nsCSPReportURI::toString(nsAString& outStr) const +{ + nsAutoCString spec; + nsresult rv = mReportURI->GetSpec(spec); + if (NS_FAILED(rv)) { + return; + } + outStr.AppendASCII(spec.get()); +} + +/* ===== nsCSPSandboxFlags ===================== */ + +nsCSPSandboxFlags::nsCSPSandboxFlags(const nsAString& aFlags) + : mFlags(aFlags) +{ + ToLowerCase(mFlags); +} + +nsCSPSandboxFlags::~nsCSPSandboxFlags() +{ +} + +bool +nsCSPSandboxFlags::visit(nsCSPSrcVisitor* aVisitor) const +{ + return false; +} + +void +nsCSPSandboxFlags::toString(nsAString& outStr) const +{ + outStr.Append(mFlags); +} + +/* ===== nsCSPDirective ====================== */ + +nsCSPDirective::nsCSPDirective(CSPDirective aDirective) +{ + mDirective = aDirective; +} + +nsCSPDirective::~nsCSPDirective() +{ + for (uint32_t i = 0; i < mSrcs.Length(); i++) { + delete mSrcs[i]; + } +} + +bool +nsCSPDirective::permits(nsIURI* aUri, const nsAString& aNonce, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure, bool aParserCreated) const +{ + if (CSPUTILSLOGENABLED()) { + CSPUTILSLOG(("nsCSPDirective::permits, aUri: %s", + aUri->GetSpecOrDefault().get())); + } + + for (uint32_t i = 0; i < mSrcs.Length(); i++) { + if (mSrcs[i]->permits(aUri, aNonce, aWasRedirected, aReportOnly, aUpgradeInsecure, aParserCreated)) { + return true; + } + } + return false; +} + +bool +nsCSPDirective::allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const +{ + CSPUTILSLOG(("nsCSPDirective::allows, aKeyWord: %s, a HashOrNonce: %s", + CSP_EnumToKeyword(aKeyword), NS_ConvertUTF16toUTF8(aHashOrNonce).get())); + + for (uint32_t i = 0; i < mSrcs.Length(); i++) { + if (mSrcs[i]->allows(aKeyword, aHashOrNonce, aParserCreated)) { + return true; + } + } + return false; +} + +void +nsCSPDirective::toString(nsAString& outStr) const +{ + // Append directive name + outStr.AppendASCII(CSP_CSPDirectiveToString(mDirective)); + outStr.AppendASCII(" "); + + // Append srcs + uint32_t length = mSrcs.Length(); + for (uint32_t i = 0; i < length; i++) { + mSrcs[i]->toString(outStr); + if (i != (length - 1)) { + outStr.AppendASCII(" "); + } + } +} + +void +nsCSPDirective::toDomCSPStruct(mozilla::dom::CSP& outCSP) const +{ + mozilla::dom::Sequence<nsString> srcs; + nsString src; + for (uint32_t i = 0; i < mSrcs.Length(); i++) { + src.Truncate(); + mSrcs[i]->toString(src); + srcs.AppendElement(src, mozilla::fallible); + } + + switch(mDirective) { + case nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE: + outCSP.mDefault_src.Construct(); + outCSP.mDefault_src.Value() = mozilla::Move(srcs); + return; + + case nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE: + outCSP.mScript_src.Construct(); + outCSP.mScript_src.Value() = mozilla::Move(srcs); + return; + + case nsIContentSecurityPolicy::OBJECT_SRC_DIRECTIVE: + outCSP.mObject_src.Construct(); + outCSP.mObject_src.Value() = mozilla::Move(srcs); + return; + + case nsIContentSecurityPolicy::STYLE_SRC_DIRECTIVE: + outCSP.mStyle_src.Construct(); + outCSP.mStyle_src.Value() = mozilla::Move(srcs); + return; + + case nsIContentSecurityPolicy::IMG_SRC_DIRECTIVE: + outCSP.mImg_src.Construct(); + outCSP.mImg_src.Value() = mozilla::Move(srcs); + return; + + case nsIContentSecurityPolicy::MEDIA_SRC_DIRECTIVE: + outCSP.mMedia_src.Construct(); + outCSP.mMedia_src.Value() = mozilla::Move(srcs); + return; + + case nsIContentSecurityPolicy::FRAME_SRC_DIRECTIVE: + outCSP.mFrame_src.Construct(); + outCSP.mFrame_src.Value() = mozilla::Move(srcs); + return; + + case nsIContentSecurityPolicy::FONT_SRC_DIRECTIVE: + outCSP.mFont_src.Construct(); + outCSP.mFont_src.Value() = mozilla::Move(srcs); + return; + + case nsIContentSecurityPolicy::CONNECT_SRC_DIRECTIVE: + outCSP.mConnect_src.Construct(); + outCSP.mConnect_src.Value() = mozilla::Move(srcs); + return; + + case nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE: + outCSP.mReport_uri.Construct(); + outCSP.mReport_uri.Value() = mozilla::Move(srcs); + return; + + case nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE: + outCSP.mFrame_ancestors.Construct(); + outCSP.mFrame_ancestors.Value() = mozilla::Move(srcs); + return; + + case nsIContentSecurityPolicy::WEB_MANIFEST_SRC_DIRECTIVE: + outCSP.mManifest_src.Construct(); + outCSP.mManifest_src.Value() = mozilla::Move(srcs); + return; + // not supporting REFLECTED_XSS_DIRECTIVE + + case nsIContentSecurityPolicy::BASE_URI_DIRECTIVE: + outCSP.mBase_uri.Construct(); + outCSP.mBase_uri.Value() = mozilla::Move(srcs); + return; + + case nsIContentSecurityPolicy::FORM_ACTION_DIRECTIVE: + outCSP.mForm_action.Construct(); + outCSP.mForm_action.Value() = mozilla::Move(srcs); + return; + + case nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT: + outCSP.mBlock_all_mixed_content.Construct(); + // does not have any srcs + return; + + case nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE: + outCSP.mUpgrade_insecure_requests.Construct(); + // does not have any srcs + return; + + case nsIContentSecurityPolicy::CHILD_SRC_DIRECTIVE: + outCSP.mChild_src.Construct(); + outCSP.mChild_src.Value() = mozilla::Move(srcs); + return; + + case nsIContentSecurityPolicy::SANDBOX_DIRECTIVE: + outCSP.mSandbox.Construct(); + outCSP.mSandbox.Value() = mozilla::Move(srcs); + return; + + // REFERRER_DIRECTIVE and REQUIRE_SRI_FOR are handled in nsCSPPolicy::toDomCSPStruct() + + default: + NS_ASSERTION(false, "cannot find directive to convert CSP to JSON"); + } +} + + +bool +nsCSPDirective::restrictsContentType(nsContentPolicyType aContentType) const +{ + // make sure we do not check for the default src before any other sources + if (isDefaultDirective()) { + return false; + } + return mDirective == CSP_ContentTypeToDirective(aContentType); +} + +void +nsCSPDirective::getReportURIs(nsTArray<nsString> &outReportURIs) const +{ + NS_ASSERTION((mDirective == nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE), "not a report-uri directive"); + + // append uris + nsString tmpReportURI; + for (uint32_t i = 0; i < mSrcs.Length(); i++) { + tmpReportURI.Truncate(); + mSrcs[i]->toString(tmpReportURI); + outReportURIs.AppendElement(tmpReportURI); + } +} + +bool +nsCSPDirective::visitSrcs(nsCSPSrcVisitor* aVisitor) const +{ + for (uint32_t i = 0; i < mSrcs.Length(); i++) { + if (!mSrcs[i]->visit(aVisitor)) { + return false; + } + } + return true; +} + +bool nsCSPDirective::equals(CSPDirective aDirective) const +{ + return (mDirective == aDirective); +} + +/* =============== nsCSPChildSrcDirective ============= */ + +nsCSPChildSrcDirective::nsCSPChildSrcDirective(CSPDirective aDirective) + : nsCSPDirective(aDirective) + , mHandleFrameSrc(false) +{ +} + +nsCSPChildSrcDirective::~nsCSPChildSrcDirective() +{ +} + +void nsCSPChildSrcDirective::setHandleFrameSrc() +{ + mHandleFrameSrc = true; +} + +bool nsCSPChildSrcDirective::restrictsContentType(nsContentPolicyType aContentType) const +{ + if (aContentType == nsIContentPolicy::TYPE_SUBDOCUMENT) { + return mHandleFrameSrc; + } + + return (aContentType == nsIContentPolicy::TYPE_INTERNAL_WORKER + || aContentType == nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER + || aContentType == nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER + ); +} + +bool nsCSPChildSrcDirective::equals(CSPDirective aDirective) const +{ + if (aDirective == nsIContentSecurityPolicy::FRAME_SRC_DIRECTIVE) { + return mHandleFrameSrc; + } + + return (aDirective == nsIContentSecurityPolicy::CHILD_SRC_DIRECTIVE); +} + +/* =============== nsBlockAllMixedContentDirective ============= */ + +nsBlockAllMixedContentDirective::nsBlockAllMixedContentDirective(CSPDirective aDirective) +: nsCSPDirective(aDirective) +{ +} + +nsBlockAllMixedContentDirective::~nsBlockAllMixedContentDirective() +{ +} + +void +nsBlockAllMixedContentDirective::toString(nsAString& outStr) const +{ + outStr.AppendASCII(CSP_CSPDirectiveToString( + nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT)); +} + +/* =============== nsUpgradeInsecureDirective ============= */ + +nsUpgradeInsecureDirective::nsUpgradeInsecureDirective(CSPDirective aDirective) +: nsCSPDirective(aDirective) +{ +} + +nsUpgradeInsecureDirective::~nsUpgradeInsecureDirective() +{ +} + +void +nsUpgradeInsecureDirective::toString(nsAString& outStr) const +{ + outStr.AppendASCII(CSP_CSPDirectiveToString( + nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE)); +} + +/* ===== nsRequireSRIForDirective ========================= */ + +nsRequireSRIForDirective::nsRequireSRIForDirective(CSPDirective aDirective) +: nsCSPDirective(aDirective) +{ +} + +nsRequireSRIForDirective::~nsRequireSRIForDirective() +{ +} + +void +nsRequireSRIForDirective::toString(nsAString &outStr) const +{ + outStr.AppendASCII(CSP_CSPDirectiveToString( + nsIContentSecurityPolicy::REQUIRE_SRI_FOR)); + for (uint32_t i = 0; i < mTypes.Length(); i++) { + if (mTypes[i] == nsIContentPolicy::TYPE_SCRIPT) { + outStr.AppendASCII(" script"); + } + else if (mTypes[i] == nsIContentPolicy::TYPE_STYLESHEET) { + outStr.AppendASCII(" style"); + } + } +} + +bool +nsRequireSRIForDirective::hasType(nsContentPolicyType aType) const +{ + for (uint32_t i = 0; i < mTypes.Length(); i++) { + if (mTypes[i] == aType) { + return true; + } + } + return false; +} + +bool +nsRequireSRIForDirective::restrictsContentType(const nsContentPolicyType aType) const +{ + return this->hasType(aType); +} + +bool +nsRequireSRIForDirective::allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const +{ + // can only disallow CSP_REQUIRE_SRI_FOR. + return (aKeyword != CSP_REQUIRE_SRI_FOR); +} + +/* ===== nsCSPPolicy ========================= */ + +nsCSPPolicy::nsCSPPolicy() + : mUpgradeInsecDir(nullptr) + , mReportOnly(false) +{ + CSPUTILSLOG(("nsCSPPolicy::nsCSPPolicy")); +} + +nsCSPPolicy::~nsCSPPolicy() +{ + CSPUTILSLOG(("nsCSPPolicy::~nsCSPPolicy")); + + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + delete mDirectives[i]; + } +} + +bool +nsCSPPolicy::permits(CSPDirective aDir, + nsIURI* aUri, + bool aSpecific) const +{ + nsString outp; + return this->permits(aDir, aUri, EmptyString(), false, aSpecific, false, outp); +} + +bool +nsCSPPolicy::permits(CSPDirective aDir, + nsIURI* aUri, + const nsAString& aNonce, + bool aWasRedirected, + bool aSpecific, + bool aParserCreated, + nsAString& outViolatedDirective) const +{ + if (CSPUTILSLOGENABLED()) { + CSPUTILSLOG(("nsCSPPolicy::permits, aUri: %s, aDir: %d, aSpecific: %s", + aUri->GetSpecOrDefault().get(), aDir, + aSpecific ? "true" : "false")); + } + + NS_ASSERTION(aUri, "permits needs an uri to perform the check!"); + outViolatedDirective.Truncate(); + + nsCSPDirective* defaultDir = nullptr; + + // Try to find a relevant directive + // These directive arrays are short (1-5 elements), not worth using a hashtable. + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->equals(aDir)) { + if (!mDirectives[i]->permits(aUri, aNonce, aWasRedirected, mReportOnly, + mUpgradeInsecDir, aParserCreated)) { + mDirectives[i]->toString(outViolatedDirective); + return false; + } + return true; + } + if (mDirectives[i]->isDefaultDirective()) { + defaultDir = mDirectives[i]; + } + } + + // If the above loop runs through, we haven't found a matching directive. + // Avoid relooping, just store the result of default-src while looping. + if (!aSpecific && defaultDir) { + if (!defaultDir->permits(aUri, aNonce, aWasRedirected, mReportOnly, + mUpgradeInsecDir, aParserCreated)) { + defaultDir->toString(outViolatedDirective); + return false; + } + return true; + } + + // Nothing restricts this, so we're allowing the load + // See bug 764937 + return true; +} + +bool +nsCSPPolicy::allows(nsContentPolicyType aContentType, + enum CSPKeyword aKeyword, + const nsAString& aHashOrNonce, + bool aParserCreated) const +{ + CSPUTILSLOG(("nsCSPPolicy::allows, aKeyWord: %s, a HashOrNonce: %s", + CSP_EnumToKeyword(aKeyword), NS_ConvertUTF16toUTF8(aHashOrNonce).get())); + + nsCSPDirective* defaultDir = nullptr; + + // Try to find a matching directive + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->restrictsContentType(aContentType)) { + if (mDirectives[i]->allows(aKeyword, aHashOrNonce, aParserCreated)) { + return true; + } + return false; + } + if (mDirectives[i]->isDefaultDirective()) { + defaultDir = mDirectives[i]; + } + } + + // {nonce,hash}-source should not consult default-src: + // * return false if default-src is specified + // * but allow the load if default-src is *not* specified (Bug 1198422) + if (aKeyword == CSP_NONCE || aKeyword == CSP_HASH) { + if (!defaultDir) { + return true; + } + return false; + } + + // If the above loop runs through, we haven't found a matching directive. + // Avoid relooping, just store the result of default-src while looping. + if (defaultDir) { + return defaultDir->allows(aKeyword, aHashOrNonce, aParserCreated); + } + + // Allowing the load; see Bug 885433 + // a) inline scripts (also unsafe eval) should only be blocked + // if there is a [script-src] or [default-src] + // b) inline styles should only be blocked + // if there is a [style-src] or [default-src] + return true; +} + +bool +nsCSPPolicy::allows(nsContentPolicyType aContentType, + enum CSPKeyword aKeyword) const +{ + return allows(aContentType, aKeyword, NS_LITERAL_STRING(""), false); +} + +void +nsCSPPolicy::toString(nsAString& outStr) const +{ + uint32_t length = mDirectives.Length(); + for (uint32_t i = 0; i < length; ++i) { + + if (mDirectives[i]->equals(nsIContentSecurityPolicy::REFERRER_DIRECTIVE)) { + outStr.AppendASCII(CSP_CSPDirectiveToString(nsIContentSecurityPolicy::REFERRER_DIRECTIVE)); + outStr.AppendASCII(" "); + outStr.Append(mReferrerPolicy); + } else { + mDirectives[i]->toString(outStr); + } + if (i != (length - 1)) { + outStr.AppendASCII("; "); + } + } +} + +void +nsCSPPolicy::toDomCSPStruct(mozilla::dom::CSP& outCSP) const +{ + outCSP.mReport_only = mReportOnly; + + for (uint32_t i = 0; i < mDirectives.Length(); ++i) { + if (mDirectives[i]->equals(nsIContentSecurityPolicy::REFERRER_DIRECTIVE)) { + mozilla::dom::Sequence<nsString> srcs; + srcs.AppendElement(mReferrerPolicy, mozilla::fallible); + outCSP.mReferrer.Construct(); + outCSP.mReferrer.Value() = srcs; + } else { + mDirectives[i]->toDomCSPStruct(outCSP); + } + } +} + +bool +nsCSPPolicy::hasDirective(CSPDirective aDir) const +{ + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->equals(aDir)) { + return true; + } + } + return false; +} + +/* + * Use this function only after ::allows() returned 'false'. Most and + * foremost it's used to get the violated directive before sending reports. + * The parameter outDirective is the equivalent of 'outViolatedDirective' + * for the ::permits() function family. + */ +void +nsCSPPolicy::getDirectiveStringForContentType(nsContentPolicyType aContentType, + nsAString& outDirective) const +{ + nsCSPDirective* defaultDir = nullptr; + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->restrictsContentType(aContentType)) { + mDirectives[i]->toString(outDirective); + return; + } + if (mDirectives[i]->isDefaultDirective()) { + defaultDir = mDirectives[i]; + } + } + // if we haven't found a matching directive yet, + // the contentType must be restricted by the default directive + if (defaultDir) { + defaultDir->toString(outDirective); + return; + } + NS_ASSERTION(false, "Can not query directive string for contentType!"); + outDirective.AppendASCII("couldNotQueryViolatedDirective"); +} + +void +nsCSPPolicy::getDirectiveAsString(CSPDirective aDir, nsAString& outDirective) const +{ + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->equals(aDir)) { + mDirectives[i]->toString(outDirective); + return; + } + } +} + +/* + * Helper function that returns the underlying bit representation of sandbox + * flags. The function returns SANDBOXED_NONE if there are no sandbox + * directives. + */ +uint32_t +nsCSPPolicy::getSandboxFlags() const +{ + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->equals(nsIContentSecurityPolicy::SANDBOX_DIRECTIVE)) { + nsAutoString flags; + mDirectives[i]->toString(flags); + + if (flags.IsEmpty()) { + return SANDBOX_ALL_FLAGS; + } + + nsAttrValue attr; + attr.ParseAtomArray(flags); + + return nsContentUtils::ParseSandboxAttributeToFlags(&attr); + } + } + + return SANDBOXED_NONE; +} + +void +nsCSPPolicy::getReportURIs(nsTArray<nsString>& outReportURIs) const +{ + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->equals(nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE)) { + mDirectives[i]->getReportURIs(outReportURIs); + return; + } + } +} + +bool +nsCSPPolicy::visitDirectiveSrcs(CSPDirective aDir, nsCSPSrcVisitor* aVisitor) const +{ + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->equals(aDir)) { + return mDirectives[i]->visitSrcs(aVisitor); + } + } + return false; +} + +bool +nsCSPPolicy::requireSRIForType(nsContentPolicyType aContentType) +{ + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->equals(nsIContentSecurityPolicy::REQUIRE_SRI_FOR)) { + return static_cast<nsRequireSRIForDirective*>(mDirectives[i])->hasType(aContentType); + } + } + return false; +} diff --git a/dom/security/nsCSPUtils.h b/dom/security/nsCSPUtils.h new file mode 100644 index 000000000..b33c8932a --- /dev/null +++ b/dom/security/nsCSPUtils.h @@ -0,0 +1,642 @@ +/* -*- 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 nsCSPUtils_h___ +#define nsCSPUtils_h___ + +#include "nsCOMPtr.h" +#include "nsIContentPolicy.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIURI.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsUnicharUtils.h" +#include "mozilla/Logging.h" + +namespace mozilla { +namespace dom { + struct CSP; +} // namespace dom +} // namespace mozilla + +/* =============== Logging =================== */ + +void CSP_LogLocalizedStr(const char16_t* aName, + const char16_t** aParams, + uint32_t aLength, + const nsAString& aSourceName, + const nsAString& aSourceLine, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aFlags, + const char* aCategory, + uint64_t aInnerWindowID); + +void CSP_GetLocalizedStr(const char16_t* aName, + const char16_t** aParams, + uint32_t aLength, + char16_t** outResult); + +void CSP_LogStrMessage(const nsAString& aMsg); + +void CSP_LogMessage(const nsAString& aMessage, + const nsAString& aSourceName, + const nsAString& aSourceLine, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aFlags, + const char* aCategory, + uint64_t aInnerWindowID); + + +/* =============== Constant and Type Definitions ================== */ + +#define INLINE_STYLE_VIOLATION_OBSERVER_TOPIC "violated base restriction: Inline Stylesheets will not apply" +#define INLINE_SCRIPT_VIOLATION_OBSERVER_TOPIC "violated base restriction: Inline Scripts will not execute" +#define EVAL_VIOLATION_OBSERVER_TOPIC "violated base restriction: Code will not be created from strings" +#define SCRIPT_NONCE_VIOLATION_OBSERVER_TOPIC "Inline Script had invalid nonce" +#define STYLE_NONCE_VIOLATION_OBSERVER_TOPIC "Inline Style had invalid nonce" +#define SCRIPT_HASH_VIOLATION_OBSERVER_TOPIC "Inline Script had invalid hash" +#define STYLE_HASH_VIOLATION_OBSERVER_TOPIC "Inline Style had invalid hash" +#define REQUIRE_SRI_SCRIPT_VIOLATION_OBSERVER_TOPIC "Missing required Subresource Integrity for Script" +#define REQUIRE_SRI_STYLE_VIOLATION_OBSERVER_TOPIC "Missing required Subresource Integrity for Style" + +// these strings map to the CSPDirectives in nsIContentSecurityPolicy +// NOTE: When implementing a new directive, you will need to add it here but also +// add a corresponding entry to the constants in nsIContentSecurityPolicy.idl +// and also create an entry for the new directive in +// nsCSPDirective::toDomCSPStruct() and add it to CSPDictionaries.webidl. +// Order of elements below important! Make sure it matches the order as in +// nsIContentSecurityPolicy.idl +static const char* CSPStrDirectives[] = { + "-error-", // NO_DIRECTIVE + "default-src", // DEFAULT_SRC_DIRECTIVE + "script-src", // SCRIPT_SRC_DIRECTIVE + "object-src", // OBJECT_SRC_DIRECTIVE + "style-src", // STYLE_SRC_DIRECTIVE + "img-src", // IMG_SRC_DIRECTIVE + "media-src", // MEDIA_SRC_DIRECTIVE + "frame-src", // FRAME_SRC_DIRECTIVE + "font-src", // FONT_SRC_DIRECTIVE + "connect-src", // CONNECT_SRC_DIRECTIVE + "report-uri", // REPORT_URI_DIRECTIVE + "frame-ancestors", // FRAME_ANCESTORS_DIRECTIVE + "reflected-xss", // REFLECTED_XSS_DIRECTIVE + "base-uri", // BASE_URI_DIRECTIVE + "form-action", // FORM_ACTION_DIRECTIVE + "referrer", // REFERRER_DIRECTIVE + "manifest-src", // MANIFEST_SRC_DIRECTIVE + "upgrade-insecure-requests", // UPGRADE_IF_INSECURE_DIRECTIVE + "child-src", // CHILD_SRC_DIRECTIVE + "block-all-mixed-content", // BLOCK_ALL_MIXED_CONTENT + "require-sri-for", // REQUIRE_SRI_FOR + "sandbox" // SANDBOX_DIRECTIVE +}; + +inline const char* CSP_CSPDirectiveToString(CSPDirective aDir) +{ + return CSPStrDirectives[static_cast<uint32_t>(aDir)]; +} + +inline CSPDirective CSP_StringToCSPDirective(const nsAString& aDir) +{ + nsString lowerDir = PromiseFlatString(aDir); + ToLowerCase(lowerDir); + + uint32_t numDirs = (sizeof(CSPStrDirectives) / sizeof(CSPStrDirectives[0])); + for (uint32_t i = 1; i < numDirs; i++) { + if (lowerDir.EqualsASCII(CSPStrDirectives[i])) { + return static_cast<CSPDirective>(i); + } + } + NS_ASSERTION(false, "Can not convert unknown Directive to Integer"); + return nsIContentSecurityPolicy::NO_DIRECTIVE; +} + +// Please add any new enum items not only to CSPKeyword, but also add +// a string version for every enum >> using the same index << to +// CSPStrKeywords underneath. +enum CSPKeyword { + CSP_SELF = 0, + CSP_UNSAFE_INLINE, + CSP_UNSAFE_EVAL, + CSP_NONE, + CSP_NONCE, + CSP_REQUIRE_SRI_FOR, + CSP_STRICT_DYNAMIC, + // CSP_LAST_KEYWORD_VALUE always needs to be the last element in the enum + // because we use it to calculate the size for the char* array. + CSP_LAST_KEYWORD_VALUE, + // Putting CSP_HASH after the delimitor, because CSP_HASH is not a valid + // keyword (hash uses e.g. sha256, sha512) but we use CSP_HASH internally + // to identify allowed hashes in ::allows. + CSP_HASH + }; + +static const char* CSPStrKeywords[] = { + "'self'", // CSP_SELF = 0 + "'unsafe-inline'", // CSP_UNSAFE_INLINE + "'unsafe-eval'", // CSP_UNSAFE_EVAL + "'none'", // CSP_NONE + "'nonce-", // CSP_NONCE + "require-sri-for", // CSP_REQUIRE_SRI_FOR + "'strict-dynamic'" // CSP_STRICT_DYNAMIC + // Remember: CSP_HASH is not supposed to be used +}; + +inline const char* CSP_EnumToKeyword(enum CSPKeyword aKey) +{ + // Make sure all elements in enum CSPKeyword got added to CSPStrKeywords. + static_assert((sizeof(CSPStrKeywords) / sizeof(CSPStrKeywords[0]) == + static_cast<uint32_t>(CSP_LAST_KEYWORD_VALUE)), + "CSP_LAST_KEYWORD_VALUE does not match length of CSPStrKeywords"); + + if (static_cast<uint32_t>(aKey) < static_cast<uint32_t>(CSP_LAST_KEYWORD_VALUE)) { + return CSPStrKeywords[static_cast<uint32_t>(aKey)]; + } + return "error: invalid keyword in CSP_EnumToKeyword"; +} + +inline CSPKeyword CSP_KeywordToEnum(const nsAString& aKey) +{ + nsString lowerKey = PromiseFlatString(aKey); + ToLowerCase(lowerKey); + + static_assert(CSP_LAST_KEYWORD_VALUE == + (sizeof(CSPStrKeywords) / sizeof(CSPStrKeywords[0])), + "CSP_LAST_KEYWORD_VALUE does not match length of CSPStrKeywords"); + + for (uint32_t i = 0; i < CSP_LAST_KEYWORD_VALUE; i++) { + if (lowerKey.EqualsASCII(CSPStrKeywords[i])) { + return static_cast<CSPKeyword>(i); + } + } + NS_ASSERTION(false, "Can not convert unknown Keyword to Enum"); + return CSP_LAST_KEYWORD_VALUE; +} + +nsresult CSP_AppendCSPFromHeader(nsIContentSecurityPolicy* aCsp, + const nsAString& aHeaderValue, + bool aReportOnly); + +/* =============== Helpers ================== */ + +class nsCSPHostSrc; + +nsCSPHostSrc* CSP_CreateHostSrcFromURI(nsIURI* aURI); +bool CSP_IsValidDirective(const nsAString& aDir); +bool CSP_IsDirective(const nsAString& aValue, CSPDirective aDir); +bool CSP_IsKeyword(const nsAString& aValue, enum CSPKeyword aKey); +bool CSP_IsQuotelessKeyword(const nsAString& aKey); +CSPDirective CSP_ContentTypeToDirective(nsContentPolicyType aType); + +class nsCSPSrcVisitor; + +void CSP_PercentDecodeStr(const nsAString& aEncStr, nsAString& outDecStr); + +/* =============== nsCSPSrc ================== */ + +class nsCSPBaseSrc { + public: + nsCSPBaseSrc(); + virtual ~nsCSPBaseSrc(); + + virtual bool permits(nsIURI* aUri, const nsAString& aNonce, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure, bool aParserCreated) const; + virtual bool allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const; + virtual bool visit(nsCSPSrcVisitor* aVisitor) const = 0; + virtual void toString(nsAString& outStr) const = 0; + + virtual void invalidate() const + { mInvalidated = true; } + + protected: + // invalidate srcs if 'script-dynamic' is present or also invalidate + // unsafe-inline' if nonce- or hash-source specified + mutable bool mInvalidated; + +}; + +/* =============== nsCSPSchemeSrc ============ */ + +class nsCSPSchemeSrc : public nsCSPBaseSrc { + public: + explicit nsCSPSchemeSrc(const nsAString& aScheme); + virtual ~nsCSPSchemeSrc(); + + bool permits(nsIURI* aUri, const nsAString& aNonce, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure, bool aParserCreated) const; + bool visit(nsCSPSrcVisitor* aVisitor) const; + void toString(nsAString& outStr) const; + + inline void getScheme(nsAString& outStr) const + { outStr.Assign(mScheme); }; + + private: + nsString mScheme; +}; + +/* =============== nsCSPHostSrc ============== */ + +class nsCSPHostSrc : public nsCSPBaseSrc { + public: + explicit nsCSPHostSrc(const nsAString& aHost); + virtual ~nsCSPHostSrc(); + + bool permits(nsIURI* aUri, const nsAString& aNonce, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure, bool aParserCreated) const; + bool visit(nsCSPSrcVisitor* aVisitor) const; + void toString(nsAString& outStr) const; + + void setScheme(const nsAString& aScheme); + void setPort(const nsAString& aPort); + void appendPath(const nsAString &aPath); + + inline void getScheme(nsAString& outStr) const + { outStr.Assign(mScheme); }; + + inline void getHost(nsAString& outStr) const + { outStr.Assign(mHost); }; + + inline void getPort(nsAString& outStr) const + { outStr.Assign(mPort); }; + + inline void getPath(nsAString& outStr) const + { outStr.Assign(mPath); }; + + private: + nsString mScheme; + nsString mHost; + nsString mPort; + nsString mPath; +}; + +/* =============== nsCSPKeywordSrc ============ */ + +class nsCSPKeywordSrc : public nsCSPBaseSrc { + public: + explicit nsCSPKeywordSrc(CSPKeyword aKeyword); + virtual ~nsCSPKeywordSrc(); + + bool allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const; + bool permits(nsIURI* aUri, const nsAString& aNonce, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure, bool aParserCreated) const; + bool visit(nsCSPSrcVisitor* aVisitor) const; + void toString(nsAString& outStr) const; + + inline CSPKeyword getKeyword() const + { return mKeyword; }; + + inline void invalidate() const + { + // keywords that need to invalidated + if (mKeyword == CSP_SELF || mKeyword == CSP_UNSAFE_INLINE) { + mInvalidated = true; + } + } + + private: + CSPKeyword mKeyword; +}; + +/* =============== nsCSPNonceSource =========== */ + +class nsCSPNonceSrc : public nsCSPBaseSrc { + public: + explicit nsCSPNonceSrc(const nsAString& aNonce); + virtual ~nsCSPNonceSrc(); + + bool permits(nsIURI* aUri, const nsAString& aNonce, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure, bool aParserCreated) const; + bool allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const; + bool visit(nsCSPSrcVisitor* aVisitor) const; + void toString(nsAString& outStr) const; + + inline void getNonce(nsAString& outStr) const + { outStr.Assign(mNonce); }; + + inline void invalidate() const + { + // overwrite nsCSPBaseSRC::invalidate() and explicitily + // do *not* invalidate, because 'strict-dynamic' should + // not invalidate nonces. + } + + private: + nsString mNonce; +}; + +/* =============== nsCSPHashSource ============ */ + +class nsCSPHashSrc : public nsCSPBaseSrc { + public: + nsCSPHashSrc(const nsAString& algo, const nsAString& hash); + virtual ~nsCSPHashSrc(); + + bool allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const; + void toString(nsAString& outStr) const; + bool visit(nsCSPSrcVisitor* aVisitor) const; + + inline void getAlgorithm(nsAString& outStr) const + { outStr.Assign(mAlgorithm); }; + + inline void getHash(nsAString& outStr) const + { outStr.Assign(mHash); }; + + inline void invalidate() const + { + // overwrite nsCSPBaseSRC::invalidate() and explicitily + // do *not* invalidate, because 'strict-dynamic' should + // not invalidate hashes. + } + + private: + nsString mAlgorithm; + nsString mHash; +}; + +/* =============== nsCSPReportURI ============ */ + +class nsCSPReportURI : public nsCSPBaseSrc { + public: + explicit nsCSPReportURI(nsIURI* aURI); + virtual ~nsCSPReportURI(); + + bool visit(nsCSPSrcVisitor* aVisitor) const; + void toString(nsAString& outStr) const; + + private: + nsCOMPtr<nsIURI> mReportURI; +}; + +/* =============== nsCSPSandboxFlags ================== */ + +class nsCSPSandboxFlags : public nsCSPBaseSrc { + public: + explicit nsCSPSandboxFlags(const nsAString& aFlags); + virtual ~nsCSPSandboxFlags(); + + bool visit(nsCSPSrcVisitor* aVisitor) const; + void toString(nsAString& outStr) const; + + private: + nsString mFlags; +}; + +/* =============== nsCSPSrcVisitor ================== */ + +class nsCSPSrcVisitor { + public: + virtual bool visitSchemeSrc(const nsCSPSchemeSrc& src) = 0; + + virtual bool visitHostSrc(const nsCSPHostSrc& src) = 0; + + virtual bool visitKeywordSrc(const nsCSPKeywordSrc& src) = 0; + + virtual bool visitNonceSrc(const nsCSPNonceSrc& src) = 0; + + virtual bool visitHashSrc(const nsCSPHashSrc& src) = 0; + + protected: + explicit nsCSPSrcVisitor() {}; + virtual ~nsCSPSrcVisitor() {}; +}; + +/* =============== nsCSPDirective ============= */ + +class nsCSPDirective { + public: + explicit nsCSPDirective(CSPDirective aDirective); + virtual ~nsCSPDirective(); + + virtual bool permits(nsIURI* aUri, const nsAString& aNonce, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure, bool aParserCreated) const; + virtual bool allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const; + virtual void toString(nsAString& outStr) const; + void toDomCSPStruct(mozilla::dom::CSP& outCSP) const; + + virtual void addSrcs(const nsTArray<nsCSPBaseSrc*>& aSrcs) + { mSrcs = aSrcs; } + + virtual bool restrictsContentType(nsContentPolicyType aContentType) const; + + inline bool isDefaultDirective() const + { return mDirective == nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE; } + + virtual bool equals(CSPDirective aDirective) const; + + void getReportURIs(nsTArray<nsString> &outReportURIs) const; + + bool visitSrcs(nsCSPSrcVisitor* aVisitor) const; + + private: + CSPDirective mDirective; + nsTArray<nsCSPBaseSrc*> mSrcs; +}; + +/* =============== nsCSPChildSrcDirective ============= */ + +/* + * In CSP 2, the child-src directive covers both workers and + * subdocuments (i.e., frames and iframes). Workers were removed + * from script-src, but frames can be controlled by either child-src + * or frame-src directives, so child-src needs to know whether it should + * also restrict frames. When both are present the frame-src directive + * takes precedent. + */ +class nsCSPChildSrcDirective : public nsCSPDirective { + public: + explicit nsCSPChildSrcDirective(CSPDirective aDirective); + virtual ~nsCSPChildSrcDirective(); + + void setHandleFrameSrc(); + + virtual bool restrictsContentType(nsContentPolicyType aContentType) const; + + virtual bool equals(CSPDirective aDirective) const; + + private: + bool mHandleFrameSrc; +}; + +/* =============== nsBlockAllMixedContentDirective === */ + +class nsBlockAllMixedContentDirective : public nsCSPDirective { + public: + explicit nsBlockAllMixedContentDirective(CSPDirective aDirective); + ~nsBlockAllMixedContentDirective(); + + bool permits(nsIURI* aUri, const nsAString& aNonce, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure, bool aParserCreated) const + { return false; } + + bool permits(nsIURI* aUri) const + { return false; } + + bool allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const + { return false; } + + void toString(nsAString& outStr) const; + + void addSrcs(const nsTArray<nsCSPBaseSrc*>& aSrcs) + { MOZ_ASSERT(false, "block-all-mixed-content does not hold any srcs"); } +}; + +/* =============== nsUpgradeInsecureDirective === */ + +/* + * Upgrading insecure requests includes the following actors: + * (1) CSP: + * The CSP implementation whitelists the http-request + * in case the policy is executed in enforcement mode. + * The CSP implementation however does not allow http + * requests to succeed if executed in report-only mode. + * In such a case the CSP implementation reports the + * error back to the page. + * + * (2) MixedContent: + * The evalution of MixedContent whitelists all http + * requests with the promise that the http requests + * gets upgraded to https before any data is fetched + * from the network. + * + * (3) CORS: + * Does not consider the http request to be of a + * different origin in case the scheme is the only + * difference in otherwise matching URIs. + * + * (4) nsHttpChannel: + * Before connecting, the channel gets redirected + * to use https. + * + * (5) WebSocketChannel: + * Similar to the httpChannel, the websocketchannel + * gets upgraded from ws to wss. + */ +class nsUpgradeInsecureDirective : public nsCSPDirective { + public: + explicit nsUpgradeInsecureDirective(CSPDirective aDirective); + ~nsUpgradeInsecureDirective(); + + bool permits(nsIURI* aUri, const nsAString& aNonce, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure, bool aParserCreated) const + { return false; } + + bool permits(nsIURI* aUri) const + { return false; } + + bool allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const + { return false; } + + void toString(nsAString& outStr) const; + + void addSrcs(const nsTArray<nsCSPBaseSrc*>& aSrcs) + { MOZ_ASSERT(false, "upgrade-insecure-requests does not hold any srcs"); } +}; + +/* ===== nsRequireSRIForDirective ========================= */ + +class nsRequireSRIForDirective : public nsCSPDirective { + public: + explicit nsRequireSRIForDirective(CSPDirective aDirective); + ~nsRequireSRIForDirective(); + + void toString(nsAString& outStr) const; + + void addType(nsContentPolicyType aType) + { mTypes.AppendElement(aType); } + bool hasType(nsContentPolicyType aType) const; + bool restrictsContentType(nsContentPolicyType aType) const; + bool allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const; + + private: + nsTArray<nsContentPolicyType> mTypes; +}; + +/* =============== nsCSPPolicy ================== */ + +class nsCSPPolicy { + public: + nsCSPPolicy(); + virtual ~nsCSPPolicy(); + + bool permits(CSPDirective aDirective, + nsIURI* aUri, + const nsAString& aNonce, + bool aWasRedirected, + bool aSpecific, + bool aParserCreated, + nsAString& outViolatedDirective) const; + bool permits(CSPDirective aDir, + nsIURI* aUri, + bool aSpecific) const; + bool allows(nsContentPolicyType aContentType, + enum CSPKeyword aKeyword, + const nsAString& aHashOrNonce, + bool aParserCreated) const; + bool allows(nsContentPolicyType aContentType, + enum CSPKeyword aKeyword) const; + void toString(nsAString& outStr) const; + void toDomCSPStruct(mozilla::dom::CSP& outCSP) const; + + inline void addDirective(nsCSPDirective* aDir) + { mDirectives.AppendElement(aDir); } + + inline void addUpgradeInsecDir(nsUpgradeInsecureDirective* aDir) + { + mUpgradeInsecDir = aDir; + addDirective(aDir); + } + + bool hasDirective(CSPDirective aDir) const; + + inline void setReportOnlyFlag(bool aFlag) + { mReportOnly = aFlag; } + + inline bool getReportOnlyFlag() const + { return mReportOnly; } + + inline void setReferrerPolicy(const nsAString* aValue) + { + mReferrerPolicy = *aValue; + ToLowerCase(mReferrerPolicy); + } + + inline void getReferrerPolicy(nsAString& outPolicy) const + { outPolicy.Assign(mReferrerPolicy); } + + void getReportURIs(nsTArray<nsString> &outReportURIs) const; + + void getDirectiveStringForContentType(nsContentPolicyType aContentType, + nsAString& outDirective) const; + + void getDirectiveAsString(CSPDirective aDir, nsAString& outDirective) const; + + uint32_t getSandboxFlags() const; + + bool requireSRIForType(nsContentPolicyType aContentType); + + inline uint32_t getNumDirectives() const + { return mDirectives.Length(); } + + bool visitDirectiveSrcs(CSPDirective aDir, nsCSPSrcVisitor* aVisitor) const; + + private: + nsUpgradeInsecureDirective* mUpgradeInsecDir; + nsTArray<nsCSPDirective*> mDirectives; + bool mReportOnly; + nsString mReferrerPolicy; +}; + +#endif /* nsCSPUtils_h___ */ diff --git a/dom/security/nsContentSecurityManager.cpp b/dom/security/nsContentSecurityManager.cpp new file mode 100644 index 000000000..c4e1ed8e1 --- /dev/null +++ b/dom/security/nsContentSecurityManager.cpp @@ -0,0 +1,695 @@ +#include "nsContentSecurityManager.h" +#include "nsIChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIStreamListener.h" +#include "nsILoadInfo.h" +#include "nsContentUtils.h" +#include "nsCORSListenerProxy.h" +#include "nsIStreamListener.h" +#include "nsIDocument.h" +#include "nsMixedContentBlocker.h" + +#include "mozilla/dom/Element.h" + +NS_IMPL_ISUPPORTS(nsContentSecurityManager, + nsIContentSecurityManager, + nsIChannelEventSink) + +static nsresult +ValidateSecurityFlags(nsILoadInfo* aLoadInfo) +{ + nsSecurityFlags securityMode = aLoadInfo->GetSecurityMode(); + + if (securityMode != nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_INHERITS && + securityMode != nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED && + securityMode != nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS && + securityMode != nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL && + securityMode != nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS) { + MOZ_ASSERT(false, "need one securityflag from nsILoadInfo to perform security checks"); + return NS_ERROR_FAILURE; + } + + // all good, found the right security flags + return NS_OK; +} + +static bool SchemeIs(nsIURI* aURI, const char* aScheme) +{ + nsCOMPtr<nsIURI> baseURI = NS_GetInnermostURI(aURI); + NS_ENSURE_TRUE(baseURI, false); + + bool isScheme = false; + return NS_SUCCEEDED(baseURI->SchemeIs(aScheme, &isScheme)) && isScheme; +} + + +static bool IsImageLoadInEditorAppType(nsILoadInfo* aLoadInfo) +{ + // Editor apps get special treatment here, editors can load images + // from anywhere. This allows editor to insert images from file:// + // into documents that are being edited. + nsContentPolicyType type = aLoadInfo->InternalContentPolicyType(); + if (type != nsIContentPolicy::TYPE_INTERNAL_IMAGE && + type != nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD && + type != nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON && + type != nsIContentPolicy::TYPE_IMAGESET) { + return false; + } + + uint32_t appType = nsIDocShell::APP_TYPE_UNKNOWN; + nsINode* node = aLoadInfo->LoadingNode(); + if (!node) { + return false; + } + nsIDocument* doc = node->OwnerDoc(); + if (!doc) { + return false; + } + + nsCOMPtr<nsIDocShellTreeItem> docShellTreeItem = doc->GetDocShell(); + if (!docShellTreeItem) { + return false; + } + + nsCOMPtr<nsIDocShellTreeItem> root; + docShellTreeItem->GetRootTreeItem(getter_AddRefs(root)); + nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(root)); + if (!docShell || NS_FAILED(docShell->GetAppType(&appType))) { + appType = nsIDocShell::APP_TYPE_UNKNOWN; + } + + return appType == nsIDocShell::APP_TYPE_EDITOR; +} + +static nsresult +DoCheckLoadURIChecks(nsIURI* aURI, nsILoadInfo* aLoadInfo) +{ + // Bug 1228117: determine the correct security policy for DTD loads + if (aLoadInfo->GetExternalContentPolicyType() == nsIContentPolicy::TYPE_DTD) { + return NS_OK; + } + + if (IsImageLoadInEditorAppType(aLoadInfo)) { + return NS_OK; + } + + uint32_t flags = nsIScriptSecurityManager::STANDARD; + if (aLoadInfo->GetAllowChrome()) { + flags |= nsIScriptSecurityManager::ALLOW_CHROME; + } + if (aLoadInfo->GetDisallowScript()) { + flags |= nsIScriptSecurityManager::DISALLOW_SCRIPT; + } + + // Only call CheckLoadURIWithPrincipal() using the TriggeringPrincipal and not + // the LoadingPrincipal when SEC_ALLOW_CROSS_ORIGIN_* security flags are set, + // to allow, e.g. user stylesheets to load chrome:// URIs. + return nsContentUtils::GetSecurityManager()-> + CheckLoadURIWithPrincipal(aLoadInfo->TriggeringPrincipal(), + aURI, + flags); +} + +static bool +URIHasFlags(nsIURI* aURI, uint32_t aURIFlags) +{ + bool hasFlags; + nsresult rv = NS_URIChainHasFlags(aURI, aURIFlags, &hasFlags); + NS_ENSURE_SUCCESS(rv, false); + + return hasFlags; +} + +static nsresult +DoSOPChecks(nsIURI* aURI, nsILoadInfo* aLoadInfo, nsIChannel* aChannel) +{ + if (aLoadInfo->GetAllowChrome() && + (URIHasFlags(aURI, nsIProtocolHandler::URI_IS_UI_RESOURCE) || + SchemeIs(aURI, "moz-safe-about"))) { + // UI resources are allowed. + return DoCheckLoadURIChecks(aURI, aLoadInfo); + } + + NS_ENSURE_FALSE(NS_HasBeenCrossOrigin(aChannel, true), + NS_ERROR_DOM_BAD_URI); + + return NS_OK; +} + +static nsresult +DoCORSChecks(nsIChannel* aChannel, nsILoadInfo* aLoadInfo, + nsCOMPtr<nsIStreamListener>& aInAndOutListener) +{ + MOZ_RELEASE_ASSERT(aInAndOutListener, "can not perform CORS checks without a listener"); + + // No need to set up CORS if TriggeringPrincipal is the SystemPrincipal. + // For example, allow user stylesheets to load XBL from external files + // without requiring CORS. + if (nsContentUtils::IsSystemPrincipal(aLoadInfo->TriggeringPrincipal())) { + return NS_OK; + } + + nsIPrincipal* loadingPrincipal = aLoadInfo->LoadingPrincipal(); + RefPtr<nsCORSListenerProxy> corsListener = + new nsCORSListenerProxy(aInAndOutListener, + loadingPrincipal, + aLoadInfo->GetCookiePolicy() == + nsILoadInfo::SEC_COOKIES_INCLUDE); + // XXX: @arg: DataURIHandling::Allow + // lets use DataURIHandling::Allow for now and then decide on callsite basis. see also: + // http://mxr.mozilla.org/mozilla-central/source/dom/security/nsCORSListenerProxy.h#33 + nsresult rv = corsListener->Init(aChannel, DataURIHandling::Allow); + NS_ENSURE_SUCCESS(rv, rv); + aInAndOutListener = corsListener; + return NS_OK; +} + +static nsresult +DoContentSecurityChecks(nsIChannel* aChannel, nsILoadInfo* aLoadInfo) +{ + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsContentPolicyType contentPolicyType = + aLoadInfo->GetExternalContentPolicyType(); + nsContentPolicyType internalContentPolicyType = + aLoadInfo->InternalContentPolicyType(); + nsCString mimeTypeGuess; + nsCOMPtr<nsINode> requestingContext = nullptr; + +#ifdef DEBUG + // Don't enforce TYPE_DOCUMENT assertions for loads + // initiated by javascript tests. + bool skipContentTypeCheck = false; + skipContentTypeCheck = Preferences::GetBool("network.loadinfo.skip_type_assertion"); +#endif + + switch(contentPolicyType) { + case nsIContentPolicy::TYPE_OTHER: { + mimeTypeGuess = EmptyCString(); + requestingContext = aLoadInfo->LoadingNode(); + break; + } + + case nsIContentPolicy::TYPE_SCRIPT: { + mimeTypeGuess = NS_LITERAL_CSTRING("application/javascript"); + requestingContext = aLoadInfo->LoadingNode(); + break; + } + + case nsIContentPolicy::TYPE_IMAGE: { + mimeTypeGuess = EmptyCString(); + requestingContext = aLoadInfo->LoadingNode(); + break; + } + + case nsIContentPolicy::TYPE_STYLESHEET: { + mimeTypeGuess = NS_LITERAL_CSTRING("text/css"); + requestingContext = aLoadInfo->LoadingNode(); + break; + } + + case nsIContentPolicy::TYPE_OBJECT: { + mimeTypeGuess = EmptyCString(); + requestingContext = aLoadInfo->LoadingNode(); + break; + } + + case nsIContentPolicy::TYPE_DOCUMENT: { + MOZ_ASSERT(skipContentTypeCheck || false, "contentPolicyType not supported yet"); + break; + } + + case nsIContentPolicy::TYPE_SUBDOCUMENT: { + mimeTypeGuess = NS_LITERAL_CSTRING("text/html"); + requestingContext = aLoadInfo->LoadingNode(); + MOZ_ASSERT(!requestingContext || + requestingContext->NodeType() == nsIDOMNode::DOCUMENT_NODE, + "type_subdocument requires requestingContext of type Document"); + break; + } + + case nsIContentPolicy::TYPE_REFRESH: { + MOZ_ASSERT(false, "contentPolicyType not supported yet"); + break; + } + + case nsIContentPolicy::TYPE_XBL: { + mimeTypeGuess = EmptyCString(); + requestingContext = aLoadInfo->LoadingNode(); + break; + } + + case nsIContentPolicy::TYPE_PING: { + mimeTypeGuess = EmptyCString(); + requestingContext = aLoadInfo->LoadingNode(); + break; + } + + case nsIContentPolicy::TYPE_XMLHTTPREQUEST: { + // alias nsIContentPolicy::TYPE_DATAREQUEST: + requestingContext = aLoadInfo->LoadingNode(); + MOZ_ASSERT(!requestingContext || + requestingContext->NodeType() == nsIDOMNode::DOCUMENT_NODE, + "type_xml requires requestingContext of type Document"); + + // We're checking for the external TYPE_XMLHTTPREQUEST here in case + // an addon creates a request with that type. + if (internalContentPolicyType == + nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST || + internalContentPolicyType == + nsIContentPolicy::TYPE_XMLHTTPREQUEST) { + mimeTypeGuess = EmptyCString(); + } + else { + MOZ_ASSERT(internalContentPolicyType == + nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE, + "can not set mime type guess for unexpected internal type"); + mimeTypeGuess = NS_LITERAL_CSTRING(TEXT_EVENT_STREAM); + } + break; + } + + case nsIContentPolicy::TYPE_OBJECT_SUBREQUEST: { + mimeTypeGuess = EmptyCString(); + requestingContext = aLoadInfo->LoadingNode(); + MOZ_ASSERT(!requestingContext || + requestingContext->NodeType() == nsIDOMNode::ELEMENT_NODE, + "type_subrequest requires requestingContext of type Element"); + break; + } + + case nsIContentPolicy::TYPE_DTD: { + mimeTypeGuess = EmptyCString(); + requestingContext = aLoadInfo->LoadingNode(); + MOZ_ASSERT(!requestingContext || + requestingContext->NodeType() == nsIDOMNode::DOCUMENT_NODE, + "type_dtd requires requestingContext of type Document"); + break; + } + + case nsIContentPolicy::TYPE_FONT: { + mimeTypeGuess = EmptyCString(); + requestingContext = aLoadInfo->LoadingNode(); + break; + } + + case nsIContentPolicy::TYPE_MEDIA: { + if (internalContentPolicyType == nsIContentPolicy::TYPE_INTERNAL_TRACK) { + mimeTypeGuess = NS_LITERAL_CSTRING("text/vtt"); + } + else { + mimeTypeGuess = EmptyCString(); + } + requestingContext = aLoadInfo->LoadingNode(); + MOZ_ASSERT(!requestingContext || + requestingContext->NodeType() == nsIDOMNode::ELEMENT_NODE, + "type_media requires requestingContext of type Element"); + break; + } + + case nsIContentPolicy::TYPE_WEBSOCKET: { + // Websockets have to use the proxied URI: + // ws:// instead of http:// for CSP checks + nsCOMPtr<nsIHttpChannelInternal> httpChannelInternal + = do_QueryInterface(aChannel); + MOZ_ASSERT(httpChannelInternal); + if (httpChannelInternal) { + httpChannelInternal->GetProxyURI(getter_AddRefs(uri)); + } + mimeTypeGuess = EmptyCString(); + requestingContext = aLoadInfo->LoadingNode(); + break; + } + + case nsIContentPolicy::TYPE_CSP_REPORT: { + mimeTypeGuess = EmptyCString(); + requestingContext = aLoadInfo->LoadingNode(); + break; + } + + case nsIContentPolicy::TYPE_XSLT: { + mimeTypeGuess = NS_LITERAL_CSTRING("application/xml"); + requestingContext = aLoadInfo->LoadingNode(); + MOZ_ASSERT(!requestingContext || + requestingContext->NodeType() == nsIDOMNode::DOCUMENT_NODE, + "type_xslt requires requestingContext of type Document"); + break; + } + + case nsIContentPolicy::TYPE_BEACON: { + mimeTypeGuess = EmptyCString(); + requestingContext = aLoadInfo->LoadingNode(); + MOZ_ASSERT(!requestingContext || + requestingContext->NodeType() == nsIDOMNode::DOCUMENT_NODE, + "type_beacon requires requestingContext of type Document"); + break; + } + + case nsIContentPolicy::TYPE_FETCH: { + mimeTypeGuess = EmptyCString(); + requestingContext = aLoadInfo->LoadingNode(); + break; + } + + case nsIContentPolicy::TYPE_IMAGESET: { + mimeTypeGuess = EmptyCString(); + requestingContext = aLoadInfo->LoadingNode(); + break; + } + + case nsIContentPolicy::TYPE_WEB_MANIFEST: { + mimeTypeGuess = NS_LITERAL_CSTRING("application/manifest+json"); + requestingContext = aLoadInfo->LoadingNode(); + break; + } + + default: + // nsIContentPolicy::TYPE_INVALID + MOZ_ASSERT(false, "can not perform security check without a valid contentType"); + } + + int16_t shouldLoad = nsIContentPolicy::ACCEPT; + rv = NS_CheckContentLoadPolicy(internalContentPolicyType, + uri, + aLoadInfo->LoadingPrincipal(), + requestingContext, + mimeTypeGuess, + nullptr, //extra, + &shouldLoad, + nsContentUtils::GetContentPolicy(), + nsContentUtils::GetSecurityManager()); + NS_ENSURE_SUCCESS(rv, rv); + if (NS_CP_REJECTED(shouldLoad)) { + return NS_ERROR_CONTENT_BLOCKED; + } + + if (nsMixedContentBlocker::sSendHSTSPriming) { + rv = nsMixedContentBlocker::MarkLoadInfoForPriming(uri, + requestingContext, + aLoadInfo); + return rv; + } + + return NS_OK; +} + +/* + * Based on the security flags provided in the loadInfo of the channel, + * doContentSecurityCheck() performs the following content security checks + * before opening the channel: + * + * (1) Same Origin Policy Check (if applicable) + * (2) Allow Cross Origin but perform sanity checks whether a principal + * is allowed to access the following URL. + * (3) Perform CORS check (if applicable) + * (4) ContentPolicy checks (Content-Security-Policy, Mixed Content, ...) + * + * @param aChannel + * The channel to perform the security checks on. + * @param aInAndOutListener + * The streamListener that is passed to channel->AsyncOpen2() that is now potentially + * wrappend within nsCORSListenerProxy() and becomes the corsListener that now needs + * to be set as new streamListener on the channel. + */ +nsresult +nsContentSecurityManager::doContentSecurityCheck(nsIChannel* aChannel, + nsCOMPtr<nsIStreamListener>& aInAndOutListener) +{ + NS_ENSURE_ARG(aChannel); + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo(); + + if (!loadInfo) { + MOZ_ASSERT(false, "channel needs to have loadInfo to perform security checks"); + return NS_ERROR_UNEXPECTED; + } + + // if dealing with a redirected channel then we have already installed + // streamlistener and redirect proxies and so we are done. + if (loadInfo->GetInitialSecurityCheckDone()) { + return NS_OK; + } + + // make sure that only one of the five security flags is set in the loadinfo + // e.g. do not require same origin and allow cross origin at the same time + nsresult rv = ValidateSecurityFlags(loadInfo); + NS_ENSURE_SUCCESS(rv, rv); + + // since aChannel was openend using asyncOpen2() we have to make sure + // that redirects of that channel also get openend using asyncOpen2() + // please note that some implementations of ::AsyncOpen2 might already + // have set that flag to true (e.g. nsViewSourceChannel) in which case + // we just set the flag again. + loadInfo->SetEnforceSecurity(true); + + if (loadInfo->GetSecurityMode() == nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS) { + rv = DoCORSChecks(aChannel, loadInfo, aInAndOutListener); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = CheckChannel(aChannel); + NS_ENSURE_SUCCESS(rv, rv); + + // Perform all ContentPolicy checks (MixedContent, CSP, ...) + rv = DoContentSecurityChecks(aChannel, loadInfo); + NS_ENSURE_SUCCESS(rv, rv); + + // now lets set the initalSecurityFlag for subsequent calls + loadInfo->SetInitialSecurityCheckDone(true); + + // all security checks passed - lets allow the load + return NS_OK; +} + +NS_IMETHODIMP +nsContentSecurityManager::AsyncOnChannelRedirect(nsIChannel* aOldChannel, + nsIChannel* aNewChannel, + uint32_t aRedirFlags, + nsIAsyncVerifyRedirectCallback *aCb) +{ + nsCOMPtr<nsILoadInfo> loadInfo = aOldChannel->GetLoadInfo(); + // Are we enforcing security using LoadInfo? + if (loadInfo && loadInfo->GetEnforceSecurity()) { + nsresult rv = CheckChannel(aNewChannel); + if (NS_FAILED(rv)) { + aOldChannel->Cancel(rv); + return rv; + } + } + + // Also verify that the redirecting server is allowed to redirect to the + // given URI + nsCOMPtr<nsIPrincipal> oldPrincipal; + nsContentUtils::GetSecurityManager()-> + GetChannelResultPrincipal(aOldChannel, getter_AddRefs(oldPrincipal)); + + nsCOMPtr<nsIURI> newURI; + aNewChannel->GetURI(getter_AddRefs(newURI)); + nsCOMPtr<nsIURI> newOriginalURI; + aNewChannel->GetOriginalURI(getter_AddRefs(newOriginalURI)); + + NS_ENSURE_STATE(oldPrincipal && newURI && newOriginalURI); + + const uint32_t flags = + nsIScriptSecurityManager::LOAD_IS_AUTOMATIC_DOCUMENT_REPLACEMENT | + nsIScriptSecurityManager::DISALLOW_SCRIPT; + nsresult rv = nsContentUtils::GetSecurityManager()-> + CheckLoadURIWithPrincipal(oldPrincipal, newURI, flags); + if (NS_SUCCEEDED(rv) && newOriginalURI != newURI) { + rv = nsContentUtils::GetSecurityManager()-> + CheckLoadURIWithPrincipal(oldPrincipal, newOriginalURI, flags); + } + NS_ENSURE_SUCCESS(rv, rv); + + aCb->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +static void +AddLoadFlags(nsIRequest *aRequest, nsLoadFlags aNewFlags) +{ + nsLoadFlags flags; + aRequest->GetLoadFlags(&flags); + flags |= aNewFlags; + aRequest->SetLoadFlags(flags); +} + +/* + * Check that this channel passes all security checks. Returns an error code + * if this requesst should not be permitted. + */ +nsresult +nsContentSecurityManager::CheckChannel(nsIChannel* aChannel) +{ + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo(); + MOZ_ASSERT(loadInfo); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + // Handle cookie policies + uint32_t cookiePolicy = loadInfo->GetCookiePolicy(); + if (cookiePolicy == nsILoadInfo::SEC_COOKIES_SAME_ORIGIN) { + + // We shouldn't have the SEC_COOKIES_SAME_ORIGIN flag for top level loads + MOZ_ASSERT(loadInfo->GetExternalContentPolicyType() != + nsIContentPolicy::TYPE_DOCUMENT); + nsIPrincipal* loadingPrincipal = loadInfo->LoadingPrincipal(); + + // It doesn't matter what we pass for the third, data-inherits, argument. + // Any protocol which inherits won't pay attention to cookies anyway. + rv = loadingPrincipal->CheckMayLoad(uri, false, false); + if (NS_FAILED(rv)) { + AddLoadFlags(aChannel, nsIRequest::LOAD_ANONYMOUS); + } + } + else if (cookiePolicy == nsILoadInfo::SEC_COOKIES_OMIT) { + AddLoadFlags(aChannel, nsIRequest::LOAD_ANONYMOUS); + } + + nsSecurityFlags securityMode = loadInfo->GetSecurityMode(); + + // CORS mode is handled by nsCORSListenerProxy + if (securityMode == nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS) { + if (NS_HasBeenCrossOrigin(aChannel)) { + loadInfo->MaybeIncreaseTainting(LoadTainting::CORS); + } + return NS_OK; + } + + // Allow subresource loads if TriggeringPrincipal is the SystemPrincipal. + // For example, allow user stylesheets to load XBL from external files. + if (nsContentUtils::IsSystemPrincipal(loadInfo->TriggeringPrincipal()) && + loadInfo->GetExternalContentPolicyType() != nsIContentPolicy::TYPE_DOCUMENT && + loadInfo->GetExternalContentPolicyType() != nsIContentPolicy::TYPE_SUBDOCUMENT) { + return NS_OK; + } + + // if none of the REQUIRE_SAME_ORIGIN flags are set, then SOP does not apply + if ((securityMode == nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_INHERITS) || + (securityMode == nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED)) { + rv = DoSOPChecks(uri, loadInfo, aChannel); + NS_ENSURE_SUCCESS(rv, rv); + } + + if ((securityMode == nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS) || + (securityMode == nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL)) { + if (NS_HasBeenCrossOrigin(aChannel)) { + loadInfo->MaybeIncreaseTainting(LoadTainting::Opaque); + } + // Please note that DoCheckLoadURIChecks should only be enforced for + // cross origin requests. If the flag SEC_REQUIRE_CORS_DATA_INHERITS is set + // within the loadInfo, then then CheckLoadURIWithPrincipal is performed + // within nsCorsListenerProxy + rv = DoCheckLoadURIChecks(uri, loadInfo); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +// ==== nsIContentSecurityManager implementation ===== + +NS_IMETHODIMP +nsContentSecurityManager::PerformSecurityCheck(nsIChannel* aChannel, + nsIStreamListener* aStreamListener, + nsIStreamListener** outStreamListener) +{ + nsCOMPtr<nsIStreamListener> inAndOutListener = aStreamListener; + nsresult rv = doContentSecurityCheck(aChannel, inAndOutListener); + NS_ENSURE_SUCCESS(rv, rv); + + inAndOutListener.forget(outStreamListener); + return NS_OK; +} + +NS_IMETHODIMP +nsContentSecurityManager::IsOriginPotentiallyTrustworthy(nsIPrincipal* aPrincipal, + bool* aIsTrustWorthy) +{ + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_ARG_POINTER(aPrincipal); + NS_ENSURE_ARG_POINTER(aIsTrustWorthy); + + if (aPrincipal->GetIsSystemPrincipal()) { + *aIsTrustWorthy = true; + return NS_OK; + } + + // The following implements: + // https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy + + *aIsTrustWorthy = false; + + if (aPrincipal->GetIsNullPrincipal()) { + return NS_OK; + } + + MOZ_ASSERT(aPrincipal->GetIsCodebasePrincipal(), + "Nobody is expected to call us with an nsIExpandedPrincipal"); + + nsCOMPtr<nsIURI> uri; + aPrincipal->GetURI(getter_AddRefs(uri)); + + nsAutoCString scheme; + nsresult rv = uri->GetScheme(scheme); + if (NS_FAILED(rv)) { + return NS_OK; + } + + // Blobs are expected to inherit their principal so we don't expect to have + // a codebase principal with scheme 'blob' here. We can't assert that though + // since someone could mess with a non-blob URI to give it that scheme. + NS_WARNING_ASSERTION(!scheme.EqualsLiteral("blob"), + "IsOriginPotentiallyTrustworthy ignoring blob scheme"); + + // According to the specification, the user agent may choose to extend the + // trust to other, vendor-specific URL schemes. We use this for "resource:", + // which is technically a substituting protocol handler that is not limited to + // local resource mapping, but in practice is never mapped remotely as this + // would violate assumptions a lot of code makes. + if (scheme.EqualsLiteral("https") || + scheme.EqualsLiteral("file") || + scheme.EqualsLiteral("resource") || + scheme.EqualsLiteral("app") || + scheme.EqualsLiteral("moz-extension") || + scheme.EqualsLiteral("wss")) { + *aIsTrustWorthy = true; + return NS_OK; + } + + nsAutoCString host; + rv = uri->GetHost(host); + if (NS_FAILED(rv)) { + return NS_OK; + } + + if (host.Equals("127.0.0.1") || + host.Equals("localhost") || + host.Equals("::1")) { + *aIsTrustWorthy = true; + return NS_OK; + } + + // If a host is not considered secure according to the default algorithm, then + // check to see if it has been whitelisted by the user. We only apply this + // whitelist for network resources, i.e., those with scheme "http" or "ws". + // The pref should contain a comma-separated list of hostnames. + if (scheme.EqualsLiteral("http") || scheme.EqualsLiteral("ws")) { + nsAdoptingCString whitelist = Preferences::GetCString("dom.securecontext.whitelist"); + if (whitelist) { + nsCCharSeparatedTokenizer tokenizer(whitelist, ','); + while (tokenizer.hasMoreTokens()) { + const nsCSubstring& allowedHost = tokenizer.nextToken(); + if (host.Equals(allowedHost)) { + *aIsTrustWorthy = true; + return NS_OK; + } + } + } + } + + return NS_OK; +} diff --git a/dom/security/nsContentSecurityManager.h b/dom/security/nsContentSecurityManager.h new file mode 100644 index 000000000..912c0e89f --- /dev/null +++ b/dom/security/nsContentSecurityManager.h @@ -0,0 +1,42 @@ +/* -*- 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 nsContentSecurityManager_h___ +#define nsContentSecurityManager_h___ + +#include "nsIContentSecurityManager.h" +#include "nsIChannel.h" +#include "nsIChannelEventSink.h" + +class nsIStreamListener; + +#define NS_CONTENTSECURITYMANAGER_CONTRACTID "@mozilla.org/contentsecuritymanager;1" +// cdcc1ab8-3cea-4e6c-a294-a651fa35227f +#define NS_CONTENTSECURITYMANAGER_CID \ +{ 0xcdcc1ab8, 0x3cea, 0x4e6c, \ + { 0xa2, 0x94, 0xa6, 0x51, 0xfa, 0x35, 0x22, 0x7f } } + +class nsContentSecurityManager : public nsIContentSecurityManager + , public nsIChannelEventSink +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTSECURITYMANAGER + NS_DECL_NSICHANNELEVENTSINK + + nsContentSecurityManager() {} + + static nsresult doContentSecurityCheck(nsIChannel* aChannel, + nsCOMPtr<nsIStreamListener>& aInAndOutListener); + +private: + static nsresult CheckChannel(nsIChannel* aChannel); + + virtual ~nsContentSecurityManager() {} + +}; + +#endif /* nsContentSecurityManager_h___ */ diff --git a/dom/security/nsMixedContentBlocker.cpp b/dom/security/nsMixedContentBlocker.cpp new file mode 100644 index 000000000..a9aca5333 --- /dev/null +++ b/dom/security/nsMixedContentBlocker.cpp @@ -0,0 +1,1191 @@ +/* -*- 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 "nsMixedContentBlocker.h" + +#include "nsContentPolicyUtils.h" +#include "nsCSPContext.h" +#include "nsThreadUtils.h" +#include "nsINode.h" +#include "nsCOMPtr.h" +#include "nsIDocShell.h" +#include "nsISecurityEventSink.h" +#include "nsIWebProgressListener.h" +#include "nsContentUtils.h" +#include "nsIRequest.h" +#include "nsIDocument.h" +#include "nsIContentViewer.h" +#include "nsIChannel.h" +#include "nsIHttpChannel.h" +#include "nsIParentChannel.h" +#include "mozilla/Preferences.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsISecureBrowserUI.h" +#include "nsIDocumentLoader.h" +#include "nsIWebNavigation.h" +#include "nsLoadGroup.h" +#include "nsIScriptError.h" +#include "nsIURI.h" +#include "nsIChannelEventSink.h" +#include "nsAsyncRedirectVerifyHelper.h" +#include "mozilla/LoadInfo.h" +#include "nsISiteSecurityService.h" + +#include "mozilla/Logging.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/ipc/URIUtils.h" + + +using namespace mozilla; + +enum nsMixedContentBlockerMessageType { + eBlocked = 0x00, + eUserOverride = 0x01 +}; + +// Is mixed script blocking (fonts, plugin content, scripts, stylesheets, +// iframes, websockets, XHR) enabled? +bool nsMixedContentBlocker::sBlockMixedScript = false; + +// Is mixed display content blocking (images, audio, video, <a ping>) enabled? +bool nsMixedContentBlocker::sBlockMixedDisplay = false; + +// Do we move HSTS before mixed-content +bool nsMixedContentBlocker::sUseHSTS = false; +// Do we send an HSTS priming request +bool nsMixedContentBlocker::sSendHSTSPriming = false; +// Default HSTS Priming failure timeout to 7 days, in seconds +uint32_t nsMixedContentBlocker::sHSTSPrimingCacheTimeout = (60 * 24 * 7); + +// Fired at the document that attempted to load mixed content. The UI could +// handle this event, for example, by displaying an info bar that offers the +// choice to reload the page with mixed content permitted. +class nsMixedContentEvent : public Runnable +{ +public: + nsMixedContentEvent(nsISupports *aContext, MixedContentTypes aType, bool aRootHasSecureConnection) + : mContext(aContext), mType(aType), mRootHasSecureConnection(aRootHasSecureConnection) + {} + + NS_IMETHOD Run() override + { + NS_ASSERTION(mContext, + "You can't call this runnable without a requesting context"); + + // To update the security UI in the tab with the blocked mixed content, call + // nsISecurityEventSink::OnSecurityChange. You can get to the event sink by + // calling NS_CP_GetDocShellFromContext on the context, and QI'ing to + // nsISecurityEventSink. + + + // Mixed content was allowed and is about to load; get the document and + // set the approriate flag to true if we are about to load Mixed Active + // Content. + nsCOMPtr<nsIDocShell> docShell = NS_CP_GetDocShellFromContext(mContext); + if (!docShell) { + return NS_OK; + } + nsCOMPtr<nsIDocShellTreeItem> sameTypeRoot; + docShell->GetSameTypeRootTreeItem(getter_AddRefs(sameTypeRoot)); + NS_ASSERTION(sameTypeRoot, "No document shell root tree item from document shell tree item!"); + + // now get the document from sameTypeRoot + nsCOMPtr<nsIDocument> rootDoc = sameTypeRoot->GetDocument(); + NS_ASSERTION(rootDoc, "No root document from document shell root tree item."); + + // Get eventSink and the current security state from the docShell + nsCOMPtr<nsISecurityEventSink> eventSink = do_QueryInterface(docShell); + NS_ASSERTION(eventSink, "No eventSink from docShell."); + nsCOMPtr<nsIDocShell> rootShell = do_GetInterface(sameTypeRoot); + NS_ASSERTION(rootShell, "No root docshell from document shell root tree item."); + uint32_t state = nsIWebProgressListener::STATE_IS_BROKEN; + nsCOMPtr<nsISecureBrowserUI> securityUI; + rootShell->GetSecurityUI(getter_AddRefs(securityUI)); + // If there is no securityUI, document doesn't have a security state to + // update. But we still want to set the document flags, so we don't return + // early. + nsresult stateRV = NS_ERROR_FAILURE; + if (securityUI) { + stateRV = securityUI->GetState(&state); + } + + if (mType == eMixedScript) { + // See if the pref will change here. If it will, only then do we need to call OnSecurityChange() to update the UI. + if (rootDoc->GetHasMixedActiveContentLoaded()) { + return NS_OK; + } + rootDoc->SetHasMixedActiveContentLoaded(true); + + // Update the security UI in the tab with the allowed mixed active content + if (securityUI) { + // Bug 1182551 - before changing the security state to broken, check + // that the root is actually secure. + if (mRootHasSecureConnection) { + // reset state security flag + state = state >> 4 << 4; + // set state security flag to broken, since there is mixed content + state |= nsIWebProgressListener::STATE_IS_BROKEN; + + // If mixed display content is loaded, make sure to include that in the state. + if (rootDoc->GetHasMixedDisplayContentLoaded()) { + state |= nsIWebProgressListener::STATE_LOADED_MIXED_DISPLAY_CONTENT; + } + + eventSink->OnSecurityChange(mContext, + (state | nsIWebProgressListener::STATE_LOADED_MIXED_ACTIVE_CONTENT)); + } else { + // root not secure, mixed active content loaded in an https subframe + if (NS_SUCCEEDED(stateRV)) { + eventSink->OnSecurityChange(mContext, (state | nsIWebProgressListener::STATE_LOADED_MIXED_ACTIVE_CONTENT)); + } + } + } + + } else if (mType == eMixedDisplay) { + // See if the pref will change here. If it will, only then do we need to call OnSecurityChange() to update the UI. + if (rootDoc->GetHasMixedDisplayContentLoaded()) { + return NS_OK; + } + rootDoc->SetHasMixedDisplayContentLoaded(true); + + // Update the security UI in the tab with the allowed mixed display content. + if (securityUI) { + // Bug 1182551 - before changing the security state to broken, check + // that the root is actually secure. + if (mRootHasSecureConnection) { + // reset state security flag + state = state >> 4 << 4; + // set state security flag to broken, since there is mixed content + state |= nsIWebProgressListener::STATE_IS_BROKEN; + + // If mixed active content is loaded, make sure to include that in the state. + if (rootDoc->GetHasMixedActiveContentLoaded()) { + state |= nsIWebProgressListener::STATE_LOADED_MIXED_ACTIVE_CONTENT; + } + + eventSink->OnSecurityChange(mContext, + (state | nsIWebProgressListener::STATE_LOADED_MIXED_DISPLAY_CONTENT)); + } else { + // root not secure, mixed display content loaded in an https subframe + if (NS_SUCCEEDED(stateRV)) { + eventSink->OnSecurityChange(mContext, (state | nsIWebProgressListener::STATE_LOADED_MIXED_DISPLAY_CONTENT)); + } + } + } + } + + return NS_OK; + } +private: + // The requesting context for the content load. Generally, a DOM node from + // the document that caused the load. + nsCOMPtr<nsISupports> mContext; + + // The type of mixed content detected, e.g. active or display + const MixedContentTypes mType; + + // Indicates whether the top level load is https or not. + bool mRootHasSecureConnection; +}; + + +nsMixedContentBlocker::nsMixedContentBlocker() +{ + // Cache the pref for mixed script blocking + Preferences::AddBoolVarCache(&sBlockMixedScript, + "security.mixed_content.block_active_content"); + + // Cache the pref for mixed display blocking + Preferences::AddBoolVarCache(&sBlockMixedDisplay, + "security.mixed_content.block_display_content"); + + // Cache the pref for HSTS + Preferences::AddBoolVarCache(&sUseHSTS, + "security.mixed_content.use_hsts"); + + // Cache the pref for sending HSTS priming + Preferences::AddBoolVarCache(&sSendHSTSPriming, + "security.mixed_content.send_hsts_priming"); + + // Cache the pref for HSTS priming failure cache time + Preferences::AddUintVarCache(&sHSTSPrimingCacheTimeout, + "security.mixed_content.hsts_priming_cache_timeout"); +} + +nsMixedContentBlocker::~nsMixedContentBlocker() +{ +} + +NS_IMPL_ISUPPORTS(nsMixedContentBlocker, nsIContentPolicy, nsIChannelEventSink) + +static void +LogMixedContentMessage(MixedContentTypes aClassification, + nsIURI* aContentLocation, + nsIDocument* aRootDoc, + nsMixedContentBlockerMessageType aMessageType) +{ + nsAutoCString messageCategory; + uint32_t severityFlag; + nsAutoCString messageLookupKey; + + if (aMessageType == eBlocked) { + severityFlag = nsIScriptError::errorFlag; + messageCategory.AssignLiteral("Mixed Content Blocker"); + if (aClassification == eMixedDisplay) { + messageLookupKey.AssignLiteral("BlockMixedDisplayContent"); + } else { + messageLookupKey.AssignLiteral("BlockMixedActiveContent"); + } + } else { + severityFlag = nsIScriptError::warningFlag; + messageCategory.AssignLiteral("Mixed Content Message"); + if (aClassification == eMixedDisplay) { + messageLookupKey.AssignLiteral("LoadingMixedDisplayContent2"); + } else { + messageLookupKey.AssignLiteral("LoadingMixedActiveContent2"); + } + } + + NS_ConvertUTF8toUTF16 locationSpecUTF16(aContentLocation->GetSpecOrDefault()); + const char16_t* strings[] = { locationSpecUTF16.get() }; + nsContentUtils::ReportToConsole(severityFlag, messageCategory, aRootDoc, + nsContentUtils::eSECURITY_PROPERTIES, + messageLookupKey.get(), strings, ArrayLength(strings)); +} + +/* nsIChannelEventSink implementation + * This code is called when a request is redirected. + * We check the channel associated with the new uri is allowed to load + * in the current context + */ +NS_IMETHODIMP +nsMixedContentBlocker::AsyncOnChannelRedirect(nsIChannel* aOldChannel, + nsIChannel* aNewChannel, + uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* aCallback) +{ + nsAsyncRedirectAutoCallback autoCallback(aCallback); + + if (!aOldChannel) { + NS_ERROR("No channel when evaluating mixed content!"); + return NS_ERROR_FAILURE; + } + + // If we are in the parent process in e10s, we don't have access to the + // document node, and hence ShouldLoad will fail when we try to get + // the docShell. If that's the case, ignore mixed content checks + // on redirects in the parent. Let the child check for mixed content. + nsCOMPtr<nsIParentChannel> is_ipc_channel; + NS_QueryNotificationCallbacks(aNewChannel, is_ipc_channel); + if (is_ipc_channel) { + return NS_OK; + } + + nsresult rv; + nsCOMPtr<nsIURI> oldUri; + rv = aOldChannel->GetURI(getter_AddRefs(oldUri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> newUri; + rv = aNewChannel->GetURI(getter_AddRefs(newUri)); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the loading Info from the old channel + nsCOMPtr<nsILoadInfo> loadInfo; + rv = aOldChannel->GetLoadInfo(getter_AddRefs(loadInfo)); + NS_ENSURE_SUCCESS(rv, rv); + if (!loadInfo) { + // XXX: We want to have a loadInfo on all channels, but we don't yet. + // If an addon creates a channel, they may not set loadinfo. If that + // channel redirects from one page to another page, we would get caught + // in this code path. Hence, we have to return NS_OK. Once we have more + // confidence that all channels have loadinfo, we can change this to + // a failure. See bug 1077201. + return NS_OK; + } + + nsContentPolicyType contentPolicyType = loadInfo->InternalContentPolicyType(); + nsCOMPtr<nsIPrincipal> requestingPrincipal = loadInfo->LoadingPrincipal(); + + // Since we are calling shouldLoad() directly on redirects, we don't go through the code + // in nsContentPolicyUtils::NS_CheckContentLoadPolicy(). Hence, we have to + // duplicate parts of it here. + nsCOMPtr<nsIURI> requestingLocation; + if (requestingPrincipal) { + // We check to see if the loadingPrincipal is systemPrincipal and return + // early if it is + if (nsContentUtils::IsSystemPrincipal(requestingPrincipal)) { + return NS_OK; + } + // We set the requestingLocation from the RequestingPrincipal. + rv = requestingPrincipal->GetURI(getter_AddRefs(requestingLocation)); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsISupports> requestingContext = loadInfo->LoadingNode(); + + int16_t decision = REJECT_REQUEST; + rv = ShouldLoad(contentPolicyType, + newUri, + requestingLocation, + requestingContext, + EmptyCString(), // aMimeGuess + nullptr, // aExtra + requestingPrincipal, + &decision); + NS_ENSURE_SUCCESS(rv, rv); + + if (nsMixedContentBlocker::sSendHSTSPriming) { + // The LoadInfo passed in is for the original channel, HSTS priming needs to + // be set on the new channel, if required. If the redirect changes + // http->https, or vice-versa, the need for priming may change. + nsCOMPtr<nsILoadInfo> newLoadInfo; + rv = aNewChannel->GetLoadInfo(getter_AddRefs(newLoadInfo)); + NS_ENSURE_SUCCESS(rv, rv); + rv = nsMixedContentBlocker::MarkLoadInfoForPriming(newUri, + requestingContext, + newLoadInfo); + if (NS_FAILED(rv)) { + decision = REJECT_REQUEST; + newLoadInfo->ClearHSTSPriming(); + } + } + + // If the channel is about to load mixed content, abort the channel + if (!NS_CP_ACCEPTED(decision)) { + autoCallback.DontCallback(); + return NS_BINDING_FAILED; + } + + return NS_OK; +} + +/* This version of ShouldLoad() is non-static and called by the Content Policy + * API and AsyncOnChannelRedirect(). See nsIContentPolicy::ShouldLoad() + * for detailed description of the parameters. + */ +NS_IMETHODIMP +nsMixedContentBlocker::ShouldLoad(uint32_t aContentType, + nsIURI* aContentLocation, + nsIURI* aRequestingLocation, + nsISupports* aRequestingContext, + const nsACString& aMimeGuess, + nsISupports* aExtra, + nsIPrincipal* aRequestPrincipal, + int16_t* aDecision) +{ + // We pass in false as the first parameter to ShouldLoad(), because the + // callers of this method don't know whether the load went through cached + // image redirects. This is handled by direct callers of the static + // ShouldLoad. + nsresult rv = ShouldLoad(false, // aHadInsecureImageRedirect + aContentType, + aContentLocation, + aRequestingLocation, + aRequestingContext, + aMimeGuess, + aExtra, + aRequestPrincipal, + aDecision); + return rv; +} + +/* Static version of ShouldLoad() that contains all the Mixed Content Blocker + * logic. Called from non-static ShouldLoad(). + */ +nsresult +nsMixedContentBlocker::ShouldLoad(bool aHadInsecureImageRedirect, + uint32_t aContentType, + nsIURI* aContentLocation, + nsIURI* aRequestingLocation, + nsISupports* aRequestingContext, + const nsACString& aMimeGuess, + nsISupports* aExtra, + nsIPrincipal* aRequestPrincipal, + int16_t* aDecision) +{ + // Asserting that we are on the main thread here and hence do not have to lock + // and unlock sBlockMixedScript and sBlockMixedDisplay before reading/writing + // to them. + MOZ_ASSERT(NS_IsMainThread()); + + bool isPreload = nsContentUtils::IsPreloadType(aContentType); + + // The content policy type that we receive may be an internal type for + // scripts. Let's remember if we have seen a worker type, and reset it to the + // external type in all cases right now. + bool isWorkerType = aContentType == nsIContentPolicy::TYPE_INTERNAL_WORKER || + aContentType == nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER || + aContentType == nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER; + aContentType = nsContentUtils::InternalContentPolicyTypeToExternal(aContentType); + + // Assume active (high risk) content and blocked by default + MixedContentTypes classification = eMixedScript; + // Make decision to block/reject by default + *aDecision = REJECT_REQUEST; + + // Notes on non-obvious decisions: + // + // TYPE_DTD: A DTD can contain entity definitions that expand to scripts. + // + // TYPE_FONT: The TrueType hinting mechanism is basically a scripting + // language that gets interpreted by the operating system's font rasterizer. + // Mixed content web fonts are relatively uncommon, and we can can fall back + // to built-in fonts with minimal disruption in almost all cases. + // + // TYPE_OBJECT_SUBREQUEST could actually be either active content (e.g. a + // script that a plugin will execute) or display content (e.g. Flash video + // content). Until we have a way to determine active vs passive content + // from plugin requests (bug 836352), we will treat this as passive content. + // This is to prevent false positives from causing users to become + // desensitized to the mixed content blocker. + // + // TYPE_CSP_REPORT: High-risk because they directly leak information about + // the content of the page, and because blocking them does not have any + // negative effect on the page loading. + // + // TYPE_PING: Ping requests are POSTS, not GETs like images and media. + // Also, PING requests have no bearing on the rendering or operation of + // the page when used as designed, so even though they are lower risk than + // scripts, blocking them is basically risk-free as far as compatibility is + // concerned. + // + // TYPE_STYLESHEET: XSLT stylesheets can insert scripts. CSS positioning + // and other advanced CSS features can possibly be exploited to cause + // spoofing attacks (e.g. make a "grant permission" button look like a + // "refuse permission" button). + // + // TYPE_BEACON: Beacon requests are similar to TYPE_PING, and are blocked by + // default. + // + // TYPE_WEBSOCKET: The Websockets API requires browsers to + // reject mixed-content websockets: "If secure is false but the origin of + // the entry script has a scheme component that is itself a secure protocol, + // e.g. HTTPS, then throw a SecurityError exception." We already block mixed + // content websockets within the websockets implementation, so we don't need + // to do any blocking here, nor do we need to provide a way to undo or + // override the blocking. Websockets without TLS are very flaky anyway in the + // face of many HTTP-aware proxies. Compared to passive content, there is + // additional risk that the script using WebSockets will disclose sensitive + // information from the HTTPS page and/or eval (directly or indirectly) + // received data. + // + // TYPE_XMLHTTPREQUEST: XHR requires either same origin or CORS, so most + // mixed-content XHR will already be blocked by that check. This will also + // block HTTPS-to-HTTP XHR with CORS. The same security concerns mentioned + // above for WebSockets apply to XHR, and XHR should have the same security + // properties as WebSockets w.r.t. mixed content. XHR's handling of redirects + // amplifies these concerns. + + + static_assert(TYPE_DATAREQUEST == TYPE_XMLHTTPREQUEST, + "TYPE_DATAREQUEST is not a synonym for " + "TYPE_XMLHTTPREQUEST"); + + switch (aContentType) { + // The top-level document cannot be mixed content by definition + case TYPE_DOCUMENT: + *aDecision = ACCEPT; + return NS_OK; + // Creating insecure websocket connections in a secure page is blocked already + // in the websocket constructor. We don't need to check the blocking here + // and we don't want to un-block + case TYPE_WEBSOCKET: + *aDecision = ACCEPT; + return NS_OK; + + // Static display content is considered moderate risk for mixed content so + // these will be blocked according to the mixed display preference + case TYPE_IMAGE: + case TYPE_MEDIA: + case TYPE_OBJECT_SUBREQUEST: + classification = eMixedDisplay; + break; + + // Active content (or content with a low value/risk-of-blocking ratio) + // that has been explicitly evaluated; listed here for documentation + // purposes and to avoid the assertion and warning for the default case. + case TYPE_BEACON: + case TYPE_CSP_REPORT: + case TYPE_DTD: + case TYPE_FETCH: + case TYPE_FONT: + case TYPE_IMAGESET: + case TYPE_OBJECT: + case TYPE_SCRIPT: + case TYPE_STYLESHEET: + case TYPE_SUBDOCUMENT: + case TYPE_PING: + case TYPE_WEB_MANIFEST: + case TYPE_XBL: + case TYPE_XMLHTTPREQUEST: + case TYPE_XSLT: + case TYPE_OTHER: + break; + + + // This content policy works as a whitelist. + default: + MOZ_ASSERT(false, "Mixed content of unknown type"); + } + + // Make sure to get the URI the load started with. No need to check + // outer schemes because all the wrapping pseudo protocols inherit the + // security properties of the actual network request represented + // by the innerMost URL. + nsCOMPtr<nsIURI> innerContentLocation = NS_GetInnermostURI(aContentLocation); + if (!innerContentLocation) { + NS_ERROR("Can't get innerURI from aContentLocation"); + *aDecision = REJECT_REQUEST; + return NS_OK; + } + + /* Get the scheme of the sub-document resource to be requested. If it is + * a safe to load in an https context then mixed content doesn't apply. + * + * Check Protocol Flags to determine if scheme is safe to load: + * URI_DOES_NOT_RETURN_DATA - e.g. + * "mailto" + * URI_IS_LOCAL_RESOURCE - e.g. + * "data", + * "resource", + * "moz-icon" + * URI_INHERITS_SECURITY_CONTEXT - e.g. + * "javascript" + * URI_SAFE_TO_LOAD_IN_SECURE_CONTEXT - e.g. + * "https", + * "moz-safe-about" + * + */ + bool schemeLocal = false; + bool schemeNoReturnData = false; + bool schemeInherits = false; + bool schemeSecure = false; + if (NS_FAILED(NS_URIChainHasFlags(innerContentLocation, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE , &schemeLocal)) || + NS_FAILED(NS_URIChainHasFlags(innerContentLocation, nsIProtocolHandler::URI_DOES_NOT_RETURN_DATA, &schemeNoReturnData)) || + NS_FAILED(NS_URIChainHasFlags(innerContentLocation, nsIProtocolHandler::URI_INHERITS_SECURITY_CONTEXT, &schemeInherits)) || + NS_FAILED(NS_URIChainHasFlags(innerContentLocation, nsIProtocolHandler::URI_SAFE_TO_LOAD_IN_SECURE_CONTEXT, &schemeSecure))) { + *aDecision = REJECT_REQUEST; + return NS_ERROR_FAILURE; + } + // TYPE_IMAGE redirects are cached based on the original URI, not the final + // destination and hence cache hits for images may not have the correct + // innerContentLocation. Check if the cached hit went through an http redirect, + // and if it did, we can't treat this as a secure subresource. + if (!aHadInsecureImageRedirect && + (schemeLocal || schemeNoReturnData || schemeInherits || schemeSecure)) { + *aDecision = ACCEPT; + return NS_OK; + } + + // Since there are cases where aRequestingLocation and aRequestPrincipal are + // definitely not the owning document, we try to ignore them by extracting the + // requestingLocation in the following order: + // 1) from the aRequestingContext, either extracting + // a) the node's principal, or the + // b) script object's principal. + // 2) if aRequestingContext yields a principal but no location, we check + // if its the system principal. If it is, allow the load. + // 3) Special case handling for: + // a) speculative loads, where shouldLoad is called twice (bug 839235) + // and the first speculative load does not include a context. + // In this case we use aRequestingLocation to set requestingLocation. + // b) TYPE_CSP_REPORT which does not provide a context. In this case we + // use aRequestingLocation to set requestingLocation. + // c) content scripts from addon code that do not provide aRequestingContext + // or aRequestingLocation, but do provide aRequestPrincipal. + // If aRequestPrincipal is an expanded principal, we allow the load. + // 4) If we still end up not having a requestingLocation, we reject the load. + + nsCOMPtr<nsIPrincipal> principal; + // 1a) Try to get the principal if aRequestingContext is a node. + nsCOMPtr<nsINode> node = do_QueryInterface(aRequestingContext); + if (node) { + principal = node->NodePrincipal(); + } + + // 1b) Try using the window's script object principal if it's not a node. + if (!principal) { + nsCOMPtr<nsIScriptObjectPrincipal> scriptObjPrin = do_QueryInterface(aRequestingContext); + if (scriptObjPrin) { + principal = scriptObjPrin->GetPrincipal(); + } + } + + nsCOMPtr<nsIURI> requestingLocation; + if (principal) { + principal->GetURI(getter_AddRefs(requestingLocation)); + } + + // 2) if aRequestingContext yields a principal but no location, we check if its a system principal. + if (principal && !requestingLocation) { + if (nsContentUtils::IsSystemPrincipal(principal)) { + *aDecision = ACCEPT; + return NS_OK; + } + } + + // 3a,b) Special case handling for speculative loads and TYPE_CSP_REPORT. In + // such cases, aRequestingContext doesn't exist, so we use aRequestingLocation. + // Unfortunately we can not distinguish between speculative and normal loads here, + // otherwise we could special case this assignment. + if (!requestingLocation) { + requestingLocation = aRequestingLocation; + } + + // 3c) Special case handling for content scripts from addons code, which only + // provide a aRequestPrincipal; aRequestingContext and aRequestingLocation are + // both null; if the aRequestPrincipal is an expandedPrincipal, we allow the load. + if (!principal && !requestingLocation && aRequestPrincipal) { + nsCOMPtr<nsIExpandedPrincipal> expanded = do_QueryInterface(aRequestPrincipal); + if (expanded) { + *aDecision = ACCEPT; + return NS_OK; + } + } + + // 4) Giving up. We still don't have a requesting location, therefore we can't tell + // if this is a mixed content load. Deny to be safe. + if (!requestingLocation) { + *aDecision = REJECT_REQUEST; + return NS_OK; + } + + // Check the parent scheme. If it is not an HTTPS page then mixed content + // restrictions do not apply. + bool parentIsHttps; + nsCOMPtr<nsIURI> innerRequestingLocation = NS_GetInnermostURI(requestingLocation); + if (!innerRequestingLocation) { + NS_ERROR("Can't get innerURI from requestingLocation"); + *aDecision = REJECT_REQUEST; + return NS_OK; + } + + nsresult rv = innerRequestingLocation->SchemeIs("https", &parentIsHttps); + if (NS_FAILED(rv)) { + NS_ERROR("requestingLocation->SchemeIs failed"); + *aDecision = REJECT_REQUEST; + return NS_OK; + } + if (!parentIsHttps) { + *aDecision = ACCEPT; + return NS_OK; + } + + nsCOMPtr<nsIDocShell> docShell = NS_CP_GetDocShellFromContext(aRequestingContext); + NS_ENSURE_TRUE(docShell, NS_OK); + + // Disallow mixed content loads for workers, shared workers and service + // workers. + if (isWorkerType) { + // For workers, we can assume that we're mixed content at this point, since + // the parent is https, and the protocol associated with innerContentLocation + // doesn't map to the secure URI flags checked above. Assert this for + // sanity's sake +#ifdef DEBUG + bool isHttpsScheme = false; + rv = innerContentLocation->SchemeIs("https", &isHttpsScheme); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(!isHttpsScheme); +#endif + *aDecision = REJECT_REQUEST; + return NS_OK; + } + + // The page might have set the CSP directive 'upgrade-insecure-requests'. In such + // a case allow the http: load to succeed with the promise that the channel will + // get upgraded to https before fetching any data from the netwerk. + // Please see: nsHttpChannel::Connect() + // + // Please note that the CSP directive 'upgrade-insecure-requests' only applies to + // http: and ws: (for websockets). Websockets are not subject to mixed content + // blocking since insecure websockets are not allowed within secure pages. Hence, + // we only have to check against http: here. Skip mixed content blocking if the + // subresource load uses http: and the CSP directive 'upgrade-insecure-requests' + // is present on the page. + bool isHttpScheme = false; + rv = innerContentLocation->SchemeIs("http", &isHttpScheme); + NS_ENSURE_SUCCESS(rv, rv); + nsIDocument* document = docShell->GetDocument(); + MOZ_ASSERT(document, "Expected a document"); + if (isHttpScheme && document->GetUpgradeInsecureRequests(isPreload)) { + *aDecision = ACCEPT; + return NS_OK; + } + + // The page might have set the CSP directive 'block-all-mixed-content' which + // should block not only active mixed content loads but in fact all mixed content + // loads, see https://www.w3.org/TR/mixed-content/#strict-checking + // Block all non secure loads in case the CSP directive is present. Please note + // that at this point we already know, based on |schemeSecure| that the load is + // not secure, so we can bail out early at this point. + if (document->GetBlockAllMixedContent(isPreload)) { + // log a message to the console before returning. + nsAutoCString spec; + rv = aContentLocation->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + NS_ConvertUTF8toUTF16 reportSpec(spec); + + const char16_t* params[] = { reportSpec.get()}; + CSP_LogLocalizedStr(u"blockAllMixedContent", + params, ArrayLength(params), + EmptyString(), // aSourceFile + EmptyString(), // aScriptSample + 0, // aLineNumber + 0, // aColumnNumber + nsIScriptError::errorFlag, "CSP", + document->InnerWindowID()); + *aDecision = REJECT_REQUEST; + return NS_OK; + } + + // Determine if the rootDoc is https and if the user decided to allow Mixed Content + bool rootHasSecureConnection = false; + bool allowMixedContent = false; + bool isRootDocShell = false; + rv = docShell->GetAllowMixedContentAndConnectionData(&rootHasSecureConnection, &allowMixedContent, &isRootDocShell); + if (NS_FAILED(rv)) { + *aDecision = REJECT_REQUEST; + return rv; + } + + // Get the sameTypeRoot tree item from the docshell + nsCOMPtr<nsIDocShellTreeItem> sameTypeRoot; + docShell->GetSameTypeRootTreeItem(getter_AddRefs(sameTypeRoot)); + NS_ASSERTION(sameTypeRoot, "No root tree item from docshell!"); + + // When navigating an iframe, the iframe may be https + // but its parents may not be. Check the parents to see if any of them are https. + // If none of the parents are https, allow the load. + if (aContentType == TYPE_SUBDOCUMENT && !rootHasSecureConnection) { + + bool httpsParentExists = false; + + nsCOMPtr<nsIDocShellTreeItem> parentTreeItem; + parentTreeItem = docShell; + + while(!httpsParentExists && parentTreeItem) { + nsCOMPtr<nsIWebNavigation> parentAsNav(do_QueryInterface(parentTreeItem)); + NS_ASSERTION(parentAsNav, "No web navigation object from parent's docshell tree item"); + nsCOMPtr<nsIURI> parentURI; + + parentAsNav->GetCurrentURI(getter_AddRefs(parentURI)); + if (!parentURI) { + // if getting the URI fails, assume there is a https parent and break. + httpsParentExists = true; + break; + } + + nsCOMPtr<nsIURI> innerParentURI = NS_GetInnermostURI(parentURI); + if (!innerParentURI) { + NS_ERROR("Can't get innerURI from parentURI"); + *aDecision = REJECT_REQUEST; + return NS_OK; + } + + if (NS_FAILED(innerParentURI->SchemeIs("https", &httpsParentExists))) { + // if getting the scheme fails, assume there is a https parent and break. + httpsParentExists = true; + break; + } + + // When the parent and the root are the same, we have traversed all the way up + // the same type docshell tree. Break out of the while loop. + if(sameTypeRoot == parentTreeItem) { + break; + } + + // update the parent to the grandparent. + nsCOMPtr<nsIDocShellTreeItem> newParentTreeItem; + parentTreeItem->GetSameTypeParent(getter_AddRefs(newParentTreeItem)); + parentTreeItem = newParentTreeItem; + } // end while loop. + + if (!httpsParentExists) { + *aDecision = nsIContentPolicy::ACCEPT; + return NS_OK; + } + } + + // Get the root document from the sameTypeRoot + nsCOMPtr<nsIDocument> rootDoc = sameTypeRoot->GetDocument(); + NS_ASSERTION(rootDoc, "No root document from document shell root tree item."); + + // Get eventSink and the current security state from the docShell + nsCOMPtr<nsISecurityEventSink> eventSink = do_QueryInterface(docShell); + NS_ASSERTION(eventSink, "No eventSink from docShell."); + nsCOMPtr<nsIDocShell> rootShell = do_GetInterface(sameTypeRoot); + NS_ASSERTION(rootShell, "No root docshell from document shell root tree item."); + uint32_t state = nsIWebProgressListener::STATE_IS_BROKEN; + nsCOMPtr<nsISecureBrowserUI> securityUI; + rootShell->GetSecurityUI(getter_AddRefs(securityUI)); + // If there is no securityUI, document doesn't have a security state. + // Allow load and return early. + if (!securityUI) { + *aDecision = nsIContentPolicy::ACCEPT; + return NS_OK; + } + nsresult stateRV = securityUI->GetState(&state); + + bool doHSTSPriming = false; + if (isHttpScheme) { + bool hsts = false; + bool cached = false; + nsCOMPtr<nsISiteSecurityService> sss = + do_GetService(NS_SSSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = sss->IsSecureURI(nsISiteSecurityService::HEADER_HSTS, aContentLocation, + 0, &cached, &hsts); + NS_ENSURE_SUCCESS(rv, rv); + + if (hsts && sUseHSTS) { + // assume we will be upgraded later + *aDecision = ACCEPT; + return NS_OK; + } + + // Send a priming request if the result is not already cached and priming + // requests are allowed + if (!cached && sSendHSTSPriming) { + // add this URI as a priming location + doHSTSPriming = true; + document->AddHSTSPrimingLocation(innerContentLocation, + HSTSPrimingState::eHSTS_PRIMING_ALLOW); + *aDecision = ACCEPT; + } + } + + // At this point we know that the request is mixed content, and the only + // question is whether we block it. Record telemetry at this point as to + // whether HSTS would have fixed things by making the content location + // into an HTTPS URL. + // + // Note that we count this for redirects as well as primary requests. This + // will cause some degree of double-counting, especially when mixed content + // is not blocked (e.g., for images). For more detail, see: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1198572#c19 + // + // We do not count requests aHadInsecureImageRedirect=true, since these are + // just an artifact of the image caching system. + bool active = (classification == eMixedScript); + if (!aHadInsecureImageRedirect) { + if (XRE_IsParentProcess()) { + AccumulateMixedContentHSTS(innerContentLocation, active, doHSTSPriming); + } else { + // Ask the parent process to do the same call + mozilla::dom::ContentChild* cc = mozilla::dom::ContentChild::GetSingleton(); + if (cc) { + mozilla::ipc::URIParams uri; + SerializeURI(innerContentLocation, uri); + cc->SendAccumulateMixedContentHSTS(uri, active, doHSTSPriming); + } + } + } + + // set hasMixedContentObjectSubrequest on this object if necessary + if (aContentType == TYPE_OBJECT_SUBREQUEST) { + rootDoc->SetHasMixedContentObjectSubrequest(true); + } + + // If the content is display content, and the pref says display content should be blocked, block it. + if (sBlockMixedDisplay && classification == eMixedDisplay) { + if (allowMixedContent) { + LogMixedContentMessage(classification, aContentLocation, rootDoc, eUserOverride); + *aDecision = nsIContentPolicy::ACCEPT; + // See if mixed display content has already loaded on the page or if the state needs to be updated here. + // If mixed display hasn't loaded previously, then we need to call OnSecurityChange() to update the UI. + if (rootDoc->GetHasMixedDisplayContentLoaded()) { + return NS_OK; + } + rootDoc->SetHasMixedDisplayContentLoaded(true); + + if (rootHasSecureConnection) { + // reset state security flag + state = state >> 4 << 4; + // set state security flag to broken, since there is mixed content + state |= nsIWebProgressListener::STATE_IS_BROKEN; + + // If mixed active content is loaded, make sure to include that in the state. + if (rootDoc->GetHasMixedActiveContentLoaded()) { + state |= nsIWebProgressListener::STATE_LOADED_MIXED_ACTIVE_CONTENT; + } + + eventSink->OnSecurityChange(aRequestingContext, + (state | nsIWebProgressListener::STATE_LOADED_MIXED_DISPLAY_CONTENT)); + } else { + // User has overriden the pref and the root is not https; + // mixed display content was allowed on an https subframe. + if (NS_SUCCEEDED(stateRV)) { + eventSink->OnSecurityChange(aRequestingContext, (state | nsIWebProgressListener::STATE_LOADED_MIXED_DISPLAY_CONTENT)); + } + } + } else { + if (doHSTSPriming) { + document->AddHSTSPrimingLocation(innerContentLocation, + HSTSPrimingState::eHSTS_PRIMING_BLOCK); + *aDecision = nsIContentPolicy::ACCEPT; + } else { + *aDecision = nsIContentPolicy::REJECT_REQUEST; + } + LogMixedContentMessage(classification, aContentLocation, rootDoc, eBlocked); + if (!rootDoc->GetHasMixedDisplayContentBlocked() && NS_SUCCEEDED(stateRV)) { + rootDoc->SetHasMixedDisplayContentBlocked(true); + eventSink->OnSecurityChange(aRequestingContext, (state | nsIWebProgressListener::STATE_BLOCKED_MIXED_DISPLAY_CONTENT)); + } + } + return NS_OK; + + } else if (sBlockMixedScript && classification == eMixedScript) { + // If the content is active content, and the pref says active content should be blocked, block it + // unless the user has choosen to override the pref + if (allowMixedContent) { + LogMixedContentMessage(classification, aContentLocation, rootDoc, eUserOverride); + *aDecision = nsIContentPolicy::ACCEPT; + // See if the state will change here. If it will, only then do we need to call OnSecurityChange() to update the UI. + if (rootDoc->GetHasMixedActiveContentLoaded()) { + return NS_OK; + } + rootDoc->SetHasMixedActiveContentLoaded(true); + + if (rootHasSecureConnection) { + // reset state security flag + state = state >> 4 << 4; + // set state security flag to broken, since there is mixed content + state |= nsIWebProgressListener::STATE_IS_BROKEN; + + // If mixed display content is loaded, make sure to include that in the state. + if (rootDoc->GetHasMixedDisplayContentLoaded()) { + state |= nsIWebProgressListener::STATE_LOADED_MIXED_DISPLAY_CONTENT; + } + + eventSink->OnSecurityChange(aRequestingContext, + (state | nsIWebProgressListener::STATE_LOADED_MIXED_ACTIVE_CONTENT)); + + return NS_OK; + } else { + // User has already overriden the pref and the root is not https; + // mixed active content was allowed on an https subframe. + if (NS_SUCCEEDED(stateRV)) { + eventSink->OnSecurityChange(aRequestingContext, (state | nsIWebProgressListener::STATE_LOADED_MIXED_ACTIVE_CONTENT)); + } + return NS_OK; + } + } else { + //User has not overriden the pref by Disabling protection. Reject the request and update the security state. + if (doHSTSPriming) { + document->AddHSTSPrimingLocation(innerContentLocation, + HSTSPrimingState::eHSTS_PRIMING_BLOCK); + *aDecision = nsIContentPolicy::ACCEPT; + } else { + *aDecision = nsIContentPolicy::REJECT_REQUEST; + } + LogMixedContentMessage(classification, aContentLocation, rootDoc, eBlocked); + // See if the pref will change here. If it will, only then do we need to call OnSecurityChange() to update the UI. + if (rootDoc->GetHasMixedActiveContentBlocked()) { + return NS_OK; + } + rootDoc->SetHasMixedActiveContentBlocked(true); + + // The user has not overriden the pref, so make sure they still have an option by calling eventSink + // which will invoke the doorhanger + if (NS_SUCCEEDED(stateRV)) { + eventSink->OnSecurityChange(aRequestingContext, (state | nsIWebProgressListener::STATE_BLOCKED_MIXED_ACTIVE_CONTENT)); + } + return NS_OK; + } + } else { + // The content is not blocked by the mixed content prefs. + + // Log a message that we are loading mixed content. + LogMixedContentMessage(classification, aContentLocation, rootDoc, eUserOverride); + + // Fire the event from a script runner as it is unsafe to run script + // from within ShouldLoad + nsContentUtils::AddScriptRunner( + new nsMixedContentEvent(aRequestingContext, classification, rootHasSecureConnection)); + *aDecision = ACCEPT; + return NS_OK; + } +} + +NS_IMETHODIMP +nsMixedContentBlocker::ShouldProcess(uint32_t aContentType, + nsIURI* aContentLocation, + nsIURI* aRequestingLocation, + nsISupports* aRequestingContext, + const nsACString& aMimeGuess, + nsISupports* aExtra, + nsIPrincipal* aRequestPrincipal, + int16_t* aDecision) +{ + aContentType = nsContentUtils::InternalContentPolicyTypeToExternal(aContentType); + + if (!aContentLocation) { + // aContentLocation may be null when a plugin is loading without an associated URI resource + if (aContentType == TYPE_OBJECT) { + *aDecision = ACCEPT; + return NS_OK; + } else { + *aDecision = REJECT_REQUEST; + return NS_ERROR_FAILURE; + } + } + + return ShouldLoad(aContentType, aContentLocation, aRequestingLocation, + aRequestingContext, aMimeGuess, aExtra, aRequestPrincipal, + aDecision); +} + +enum MixedContentHSTSState { + MCB_HSTS_PASSIVE_NO_HSTS = 0, + MCB_HSTS_PASSIVE_WITH_HSTS = 1, + MCB_HSTS_ACTIVE_NO_HSTS = 2, + MCB_HSTS_ACTIVE_WITH_HSTS = 3 +}; + +// Similar to the existing mixed-content HSTS, except MCB_HSTS_*_NO_HSTS is +// broken into two distinct states, indicating whether we plan to send a priming +// request or not. If we decided not go send a priming request, it could be +// because it is a type we do not support, or because we cached a previous +// negative response. +enum MixedContentHSTSPrimingState { + eMCB_HSTS_PASSIVE_WITH_HSTS = 0, + eMCB_HSTS_ACTIVE_WITH_HSTS = 1, + eMCB_HSTS_PASSIVE_NO_PRIMING = 2, + eMCB_HSTS_PASSIVE_DO_PRIMING = 3, + eMCB_HSTS_ACTIVE_NO_PRIMING = 4, + eMCB_HSTS_ACTIVE_DO_PRIMING = 5 +}; + +// Record information on when HSTS would have made mixed content not mixed +// content (regardless of whether it was actually blocked) +void +nsMixedContentBlocker::AccumulateMixedContentHSTS(nsIURI* aURI, bool aActive, bool aHasHSTSPriming) +{ + // This method must only be called in the parent, because + // nsSiteSecurityService is only available in the parent + if (!XRE_IsParentProcess()) { + MOZ_ASSERT(false); + return; + } + + bool hsts; + nsresult rv; + nsCOMPtr<nsISiteSecurityService> sss = do_GetService(NS_SSSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + return; + } + rv = sss->IsSecureURI(nsISiteSecurityService::HEADER_HSTS, aURI, 0, nullptr, &hsts); + if (NS_FAILED(rv)) { + return; + } + + // states: would upgrade, would prime, hsts info cached + // active, passive + // + if (!aActive) { + if (!hsts) { + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS, + MCB_HSTS_PASSIVE_NO_HSTS); + if (aHasHSTSPriming) { + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING, + eMCB_HSTS_PASSIVE_DO_PRIMING); + } else { + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING, + eMCB_HSTS_PASSIVE_NO_PRIMING); + } + } + else { + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS, + MCB_HSTS_PASSIVE_WITH_HSTS); + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING, + eMCB_HSTS_PASSIVE_WITH_HSTS); + } + } else { + if (!hsts) { + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS, + MCB_HSTS_ACTIVE_NO_HSTS); + if (aHasHSTSPriming) { + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING, + eMCB_HSTS_ACTIVE_DO_PRIMING); + } else { + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING, + eMCB_HSTS_ACTIVE_NO_PRIMING); + } + } + else { + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS, + MCB_HSTS_ACTIVE_WITH_HSTS); + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING, + eMCB_HSTS_ACTIVE_WITH_HSTS); + } + } +} + +//static +nsresult +nsMixedContentBlocker::MarkLoadInfoForPriming(nsIURI* aURI, + nsISupports* aRequestingContext, + nsILoadInfo* aLoadInfo) +{ + nsresult rv; + bool sendPriming = false; + bool mixedContentWouldBlock = false; + rv = GetHSTSPrimingFromRequestingContext(aURI, + aRequestingContext, + &sendPriming, + &mixedContentWouldBlock); + NS_ENSURE_SUCCESS(rv, rv); + + if (sendPriming) { + aLoadInfo->SetHSTSPriming(mixedContentWouldBlock); + } + + return NS_OK; +} + +//static +nsresult +nsMixedContentBlocker::GetHSTSPrimingFromRequestingContext(nsIURI* aURI, + nsISupports* aRequestingContext, + bool* aSendPrimingRequest, + bool* aMixedContentWouldBlock) +{ + *aSendPrimingRequest = false; + *aMixedContentWouldBlock = false; + // If we marked for priming, we used the innermost URI, so get that + nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(aURI); + if (!innerURI) { + NS_ERROR("Can't get innerURI from aContentLocation"); + return NS_ERROR_CONTENT_BLOCKED; + } + + bool isHttp = false; + innerURI->SchemeIs("http", &isHttp); + if (!isHttp) { + // there is nothign to do + return NS_OK; + } + + // If the DocShell was marked for HSTS priming, propagate that to the LoadInfo + nsCOMPtr<nsIDocShell> docShell = NS_CP_GetDocShellFromContext(aRequestingContext); + if (!docShell) { + return NS_OK; + } + nsCOMPtr<nsIDocument> document = docShell->GetDocument(); + if (!document) { + return NS_OK; + } + + HSTSPrimingState status = document->GetHSTSPrimingStateForLocation(innerURI); + if (status != HSTSPrimingState::eNO_HSTS_PRIMING) { + *aSendPrimingRequest = (status != HSTSPrimingState::eNO_HSTS_PRIMING); + *aMixedContentWouldBlock = (status == HSTSPrimingState::eHSTS_PRIMING_BLOCK); + } + + return NS_OK; +} diff --git a/dom/security/nsMixedContentBlocker.h b/dom/security/nsMixedContentBlocker.h new file mode 100644 index 000000000..539c3ebbb --- /dev/null +++ b/dom/security/nsMixedContentBlocker.h @@ -0,0 +1,105 @@ +/* -*- 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 nsMixedContentBlocker_h___ +#define nsMixedContentBlocker_h___ + +#define NS_MIXEDCONTENTBLOCKER_CONTRACTID "@mozilla.org/mixedcontentblocker;1" +/* daf1461b-bf29-4f88-8d0e-4bcdf332c862 */ +#define NS_MIXEDCONTENTBLOCKER_CID \ +{ 0xdaf1461b, 0xbf29, 0x4f88, \ + { 0x8d, 0x0e, 0x4b, 0xcd, 0xf3, 0x32, 0xc8, 0x62 } } + +// This enum defines type of content that is detected when an +// nsMixedContentEvent fires +enum MixedContentTypes { + // "Active" content, such as fonts, plugin content, JavaScript, stylesheets, + // iframes, WebSockets, and XHR + eMixedScript, + // "Display" content, such as images, audio, video, and <a ping> + eMixedDisplay +}; + +#include "nsIContentPolicy.h" +#include "nsIChannel.h" +#include "nsIChannelEventSink.h" +#include "imgRequest.h" + +class nsILoadInfo; // forward declaration + +class nsMixedContentBlocker : public nsIContentPolicy, + public nsIChannelEventSink +{ +private: + virtual ~nsMixedContentBlocker(); + +public: + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTPOLICY + NS_DECL_NSICHANNELEVENTSINK + + nsMixedContentBlocker(); + + /* Static version of ShouldLoad() that contains all the Mixed Content Blocker + * logic. Called from non-static ShouldLoad(). + * Called directly from imageLib when an insecure redirect exists in a cached + * image load. + * @param aHadInsecureImageRedirect + * boolean flag indicating that an insecure redirect through http + * occured when this image was initially loaded and cached. + * Remaining parameters are from nsIContentPolicy::ShouldLoad(). + */ + static nsresult ShouldLoad(bool aHadInsecureImageRedirect, + uint32_t aContentType, + nsIURI* aContentLocation, + nsIURI* aRequestingLocation, + nsISupports* aRequestingContext, + const nsACString& aMimeGuess, + nsISupports* aExtra, + nsIPrincipal* aRequestPrincipal, + int16_t* aDecision); + static void AccumulateMixedContentHSTS(nsIURI* aURI, + bool aActive, + bool aHasHSTSPriming); + /* If the document associated with aRequestingContext requires priming for + * aURI, propagate that to the LoadInfo so the HttpChannel will find out about + * it. + * + * @param aURI The URI associated with the load + * @param aRequestingContext the requesting context passed to ShouldLoad + * @param aLoadInfo the LoadInfo for the load + */ + static nsresult MarkLoadInfoForPriming(nsIURI* aURI, + nsISupports* aRequestingContext, + nsILoadInfo* aLoadInfo); + + /* Given a context, return whether HSTS was marked on the document associated + * with the load for the given URI. This is used by MarkLoadInfoForPriming and + * directly by the image loader to determine whether to allow a load to occur + * from the cache. + * + * @param aURI The URI associated with the load + * @param aRequestingContext the requesting context passed to ShouldLoad + * @param aSendPrimingRequest out true if priming is required on the channel + * @param aMixedContentWouldBlock out true if mixed content would block + */ + static nsresult GetHSTSPrimingFromRequestingContext(nsIURI* aURI, + nsISupports* aRequestingContext, + bool* aSendPrimingRequest, + bool* aMixedContentWouldBlock); + + + static bool sBlockMixedScript; + static bool sBlockMixedDisplay; + // Do we move HSTS before mixed-content + static bool sUseHSTS; + // Do we send an HSTS priming request + static bool sSendHSTSPriming; + // Default HSTS Priming failure timeout in seconds + static uint32_t sHSTSPrimingCacheTimeout; +}; + +#endif /* nsMixedContentBlocker_h___ */ diff --git a/dom/security/test/contentverifier/browser.ini b/dom/security/test/contentverifier/browser.ini new file mode 100644 index 000000000..c41c2e8e8 --- /dev/null +++ b/dom/security/test/contentverifier/browser.ini @@ -0,0 +1,19 @@ +[DEFAULT] +support-files = + file_contentserver.sjs + file_about_newtab.html + file_about_newtab_bad.html + file_about_newtab_bad_csp.html + file_about_newtab_bad_csp_signature + file_about_newtab_good_signature + file_about_newtab_bad_signature + file_about_newtab_broken_signature + file_about_newtab_sri.html + file_about_newtab_sri_signature + goodChain.pem + head.js + script.js + style.css + +[browser_verify_content_about_newtab.js] +[browser_verify_content_about_newtab2.js] diff --git a/dom/security/test/contentverifier/browser_verify_content_about_newtab.js b/dom/security/test/contentverifier/browser_verify_content_about_newtab.js new file mode 100644 index 000000000..f63726ef8 --- /dev/null +++ b/dom/security/test/contentverifier/browser_verify_content_about_newtab.js @@ -0,0 +1,20 @@ + +const TESTS = [ + // { newtab (aboutURI) or regular load (url) : url, + // testStrings : expected strings in the loaded page } + { "aboutURI" : URI_GOOD, "testStrings" : [GOOD_ABOUT_STRING] }, + { "aboutURI" : URI_ERROR_HEADER, "testStrings" : [ABOUT_BLANK] }, + { "url" : URI_BAD_FILE_CACHED, "testStrings" : [BAD_ABOUT_STRING] }, + { "aboutURI" : URI_BAD_FILE_CACHED, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_GOOD, "testStrings" : [GOOD_ABOUT_STRING] }, + { "aboutURI" : URI_SRI, "testStrings" : [ + STYLESHEET_WITHOUT_SRI_BLOCKED, + STYLESHEET_WITH_SRI_LOADED, + SCRIPT_WITHOUT_SRI_BLOCKED, + SCRIPT_WITH_SRI_LOADED, + ]}, + { "aboutURI" : URI_BAD_CSP, "testStrings" : [CSP_TEST_SUCCESS_STRING] }, + { "url" : URI_CLEANUP, "testStrings" : [CLEANUP_DONE] }, +]; + +add_task(runTests); diff --git a/dom/security/test/contentverifier/browser_verify_content_about_newtab2.js b/dom/security/test/contentverifier/browser_verify_content_about_newtab2.js new file mode 100644 index 000000000..a35ff6660 --- /dev/null +++ b/dom/security/test/contentverifier/browser_verify_content_about_newtab2.js @@ -0,0 +1,19 @@ + +const TESTS = [ + // { newtab (aboutURI) or regular load (url) : url, + // testStrings : expected strings in the loaded page } + { "aboutURI" : URI_GOOD, "testStrings" : [GOOD_ABOUT_STRING] }, + { "aboutURI" : URI_ERROR_HEADER, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_KEYERROR_HEADER, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_SIGERROR_HEADER, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_NO_HEADER, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_BAD_SIG, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_BROKEN_SIG, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_BAD_X5U, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_HTTP_X5U, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_BAD_FILE, "testStrings" : [ABOUT_BLANK] }, + { "aboutURI" : URI_BAD_ALL, "testStrings" : [ABOUT_BLANK] }, + { "url" : URI_CLEANUP, "testStrings" : [CLEANUP_DONE] }, +]; + +add_task(runTests); diff --git a/dom/security/test/contentverifier/file_about_newtab.html b/dom/security/test/contentverifier/file_about_newtab.html new file mode 100644 index 000000000..f274743b9 --- /dev/null +++ b/dom/security/test/contentverifier/file_about_newtab.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<!-- https://bugzilla.mozilla.org/show_bug.cgi?id=1226928 --> +<head> + <meta charset="utf-8"> + <title>Testpage for bug 1226928</title> +</head> +<body> + Just a fully good testpage for Bug 1226928<br/> +</body> +</html>
\ No newline at end of file diff --git a/dom/security/test/contentverifier/file_about_newtab_bad.html b/dom/security/test/contentverifier/file_about_newtab_bad.html new file mode 100644 index 000000000..45899f4f4 --- /dev/null +++ b/dom/security/test/contentverifier/file_about_newtab_bad.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<!-- https://bugzilla.mozilla.org/show_bug.cgi?id=1226928 --> +<head> + <meta charset="utf-8"> + <title>Testpage for bug 1226928</title> +</head> +<body> + Just a bad testpage for Bug 1226928<br/> +</body> +</html>
\ No newline at end of file diff --git a/dom/security/test/contentverifier/file_about_newtab_bad_csp.html b/dom/security/test/contentverifier/file_about_newtab_bad_csp.html new file mode 100644 index 000000000..f8044b73f --- /dev/null +++ b/dom/security/test/contentverifier/file_about_newtab_bad_csp.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Testpage for CSP violation (inline script)</title> +</head> +<body> + CSP violation test succeeded. + <script> + // This inline script would override the success string if loaded. + document.body.innerHTML = "CSP violation test failed."; + </script> +</body> +</html>
\ No newline at end of file diff --git a/dom/security/test/contentverifier/file_about_newtab_bad_csp_signature b/dom/security/test/contentverifier/file_about_newtab_bad_csp_signature new file mode 100644 index 000000000..ded42dc9f --- /dev/null +++ b/dom/security/test/contentverifier/file_about_newtab_bad_csp_signature @@ -0,0 +1 @@ +oiypz3lb-IyJsmKNsnlp2zDrqncste8yONn9WUE6ksgJWMhSEQ9lp8vRqN0W3JPwJb6uSk16RI-tDv7uy0jxon5jL1BZpqlqIpvimg7FCQEedMKoHZwtE9an-e95sOTd
\ No newline at end of file diff --git a/dom/security/test/contentverifier/file_about_newtab_bad_signature b/dom/security/test/contentverifier/file_about_newtab_bad_signature new file mode 100644 index 000000000..73a3c1e34 --- /dev/null +++ b/dom/security/test/contentverifier/file_about_newtab_bad_signature @@ -0,0 +1 @@ +KirX94omQL7lKfWGhc777t8U29enDg0O0UcJLH3PRXcvWGO8KA6mmLS3yNCFnGiTjP3vNnVtm-sUkXr4ix8WTkKABkU4fEAi77sNOkLCKw40M9sDJOesmYInS_J2AuXX
\ No newline at end of file diff --git a/dom/security/test/contentverifier/file_about_newtab_broken_signature b/dom/security/test/contentverifier/file_about_newtab_broken_signature new file mode 100644 index 000000000..468a167ff --- /dev/null +++ b/dom/security/test/contentverifier/file_about_newtab_broken_signature @@ -0,0 +1 @@ +MGUCMFwSs3o95ukwBWXN1WbLgnpJ_uHWFiQROPm9zjrSqzlfiSMyLwJwIZzldWo_pBJtOwIxAJIfhXIiMVfl5NkFEJUUMxzu6FuxOJl5DCpG2wHLy9AhayLUzm4X4SpwZ6QBPapdTg
\ No newline at end of file diff --git a/dom/security/test/contentverifier/file_about_newtab_good_signature b/dom/security/test/contentverifier/file_about_newtab_good_signature new file mode 100644 index 000000000..d826d49c3 --- /dev/null +++ b/dom/security/test/contentverifier/file_about_newtab_good_signature @@ -0,0 +1 @@ +HUndgHvxHNMiAe1SXoeyOOraUJCdxHqWkAYTu0Cq1KpAHcWZEVelNTvyXGbTLWj8btsmqNLAm08UlyK43q_2oO9DQfez3Fo8DhsKvm7TqgSXCkhUoxsYNanxWXhqw-Jw
\ No newline at end of file diff --git a/dom/security/test/contentverifier/file_about_newtab_sri.html b/dom/security/test/contentverifier/file_about_newtab_sri.html new file mode 100644 index 000000000..4415c533a --- /dev/null +++ b/dom/security/test/contentverifier/file_about_newtab_sri.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<!-- https://bugzilla.mozilla.org/show_bug.cgi?id=1235572 --> +<head> + <meta charset="utf-8"> + <title>Testpage for bug 1235572</title> + <script> + function loaded(resource) { + document.getElementById("result").innerHTML += resource + " loaded\n"; + } + function blocked(resource) { + document.getElementById("result").innerHTML += resource + " blocked\n"; + } + </script> +</head> +<body> + Testing script loading without SRI for Bug 1235572<br/> + <div id="result"></div> + + <!-- use css1 and css2 to make urls different to avoid the resource being cached--> + <link rel="stylesheet" href="https://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?resource=css1" + onload="loaded('Stylesheet without SRI')" + onerror="blocked('Stylesheet without SRI')"> + <link rel="stylesheet" href="https://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?resource=css2" + integrity="sha384-/6Tvxh7SX39y62qePcvYoi5Vrf0lK8Ix3wJFLCYKI5KNJ5wIlCR8UsFC1OXwmwgd" + onload="loaded('Stylesheet with SRI')" + onerror="blocked('Stylesheet with SRI')"> + <script src="https://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?resource=script" + onload="loaded('Script without SRI')" + onerror="blocked('Script without SRI')"></script> + <script src="https://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?resource=script" + integrity="sha384-zDCkvKOHXk8mM6Nk07oOGXGME17PA4+ydFw+hq0r9kgF6ZDYFWK3fLGPEy7FoOAo" + onload="loaded('Script with SRI')" + onerror="blocked('Script with SRI')"></script> +</body> +</html> diff --git a/dom/security/test/contentverifier/file_about_newtab_sri_signature b/dom/security/test/contentverifier/file_about_newtab_sri_signature new file mode 100644 index 000000000..b7ac17944 --- /dev/null +++ b/dom/security/test/contentverifier/file_about_newtab_sri_signature @@ -0,0 +1 @@ +yoIyAYiiEzdP1zpkRy3KaqdsjUy62Notku89cytwVwcH0x6fKsMCdM-df1wbk9N28CSTaIOW5kcSenFy5K3nU-zPIoqZDjQo6aSjF8hF6lrw1a1xbhfl9K3g4YJsuWsO
\ No newline at end of file diff --git a/dom/security/test/contentverifier/file_contentserver.sjs b/dom/security/test/contentverifier/file_contentserver.sjs new file mode 100644 index 000000000..3ea49cdde --- /dev/null +++ b/dom/security/test/contentverifier/file_contentserver.sjs @@ -0,0 +1,261 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// sjs for remote about:newtab (bug 1226928) +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.importGlobalProperties(["URLSearchParams"]); + +const path = "browser/dom/security/test/contentverifier/"; + +const goodFileName = "file_about_newtab.html"; +const goodFileBase = path + goodFileName; +const goodFile = FileUtils.getDir("TmpD", [], true); +goodFile.append(goodFileName); +const goodSignature = path + "file_about_newtab_good_signature"; +const goodX5UString = "\"https://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?x5u=default\""; + +const scriptFileName = "script.js"; +const cssFileName = "style.css"; +const badFile = path + "file_about_newtab_bad.html"; +const brokenSignature = path + "file_about_newtab_broken_signature"; +const badSignature = path + "file_about_newtab_bad_signature"; +const badX5UString = "\"https://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?x5u=bad\""; +const httpX5UString = "\"http://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?x5u=default\""; + +const sriFile = path + "file_about_newtab_sri.html"; +const sriSignature = path + "file_about_newtab_sri_signature"; + +const badCspFile = path + "file_about_newtab_bad_csp.html"; +const badCspSignature = path + "file_about_newtab_bad_csp_signature"; + +// This cert chain is copied from +// security/manager/ssl/tests/unit/test_content_signing/ +// using the certificates +// * content_signing_remote_newtab_ee.pem +// * content_signing_int.pem +// * content_signing_root.pem +const goodCertChainPath = path + "goodChain.pem"; + +const tempFileNames = [goodFileName, scriptFileName, cssFileName]; + +// we copy the file to serve as newtab to a temp directory because +// we modify it during tests. +setupTestFiles(); + +function setupTestFiles() { + for (let fileName of tempFileNames) { + let tempFile = FileUtils.getDir("TmpD", [], true); + tempFile.append(fileName); + if (!tempFile.exists()) { + let fileIn = getFileName(path + fileName, "CurWorkD"); + fileIn.copyTo(FileUtils.getDir("TmpD", [], true), ""); + } + } +} + +function getFileName(filePath, dir) { + // Since it's relative to the cwd of the test runner, we start there and + // append to get to the actual path of the file. + let testFile = + Cc["@mozilla.org/file/directory_service;1"]. + getService(Components.interfaces.nsIProperties). + get(dir, Components.interfaces.nsILocalFile); + let dirs = filePath.split("/"); + for (let i = 0; i < dirs.length; i++) { + testFile.append(dirs[i]); + } + return testFile; +} + +function loadFile(file) { + // Load a file to return it. + let testFileStream = + Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Components.interfaces.nsIFileInputStream); + testFileStream.init(file, -1, 0, 0); + return NetUtil.readInputStreamToString(testFileStream, + testFileStream.available()); +} + +function appendToFile(aFile, content) { + try { + let file = FileUtils.openFileOutputStream(aFile, FileUtils.MODE_APPEND | + FileUtils.MODE_WRONLY); + file.write(content, content.length); + file.close(); + } catch (e) { + dump(">>> Error in appendToFile "+e); + return "Error"; + } + return "Done"; +} + +function truncateFile(aFile, length) { + let fileIn = loadFile(aFile); + fileIn = fileIn.slice(0, -length); + + try { + let file = FileUtils.openFileOutputStream(aFile, FileUtils.MODE_WRONLY | + FileUtils.MODE_TRUNCATE); + file.write(fileIn, fileIn.length); + file.close(); + } catch (e) { + dump(">>> Error in truncateFile "+e); + return "Error"; + } + return "Done"; +} + +function cleanupTestFiles() { + for (let fileName of tempFileNames) { + let tempFile = FileUtils.getDir("TmpD", [], true); + tempFile.append(fileName); + tempFile.remove(true); + } +} + +/* + * handle requests of the following form: + * sig=good&key=good&file=good&header=good&cached=no to serve pages with + * content signatures + * + * it further handles invalidateFile=yep and validateFile=yep to change the + * served file + */ +function handleRequest(request, response) { + let params = new URLSearchParams(request.queryString); + let x5uType = params.get("x5u"); + let signatureType = params.get("sig"); + let fileType = params.get("file"); + let headerType = params.get("header"); + let cached = params.get("cached"); + let invalidateFile = params.get("invalidateFile"); + let validateFile = params.get("validateFile"); + let resource = params.get("resource"); + let x5uParam = params.get("x5u"); + + if (params.get("cleanup")) { + cleanupTestFiles(); + response.setHeader("Content-Type", "text/html", false); + response.write("Done"); + return; + } + + if (resource) { + if (resource == "script") { + response.setHeader("Content-Type", "application/javascript", false); + response.write(loadFile(getFileName(scriptFileName, "TmpD"))); + } else { // resource == "css1" || resource == "css2" + response.setHeader("Content-Type", "text/css", false); + response.write(loadFile(getFileName(cssFileName, "TmpD"))); + } + return; + } + + // if invalidateFile is set, this doesn't actually return a newtab page + // but changes the served file to invalidate the signature + // NOTE: make sure to make the file valid again afterwards! + if (invalidateFile) { + let r = "Done"; + for (let fileName of tempFileNames) { + if (appendToFile(getFileName(fileName, "TmpD"), "!") != "Done") { + r = "Error"; + } + } + response.setHeader("Content-Type", "text/html", false); + response.write(r); + return; + } + + // if validateFile is set, this doesn't actually return a newtab page + // but changes the served file to make the signature valid again + if (validateFile) { + let r = "Done"; + for (let fileName of tempFileNames) { + if (truncateFile(getFileName(fileName, "TmpD"), 1) != "Done") { + r = "Error"; + } + } + response.setHeader("Content-Type", "text/html", false); + response.write(r); + return; + } + + // we have to return the certificate chain on request for the x5u parameter + if (x5uParam && x5uParam == "default") { + response.setHeader("Cache-Control", "max-age=216000", false); + response.setHeader("Content-Type", "text/plain", false); + response.write(loadFile(getFileName(goodCertChainPath, "CurWorkD"))); + return; + } + + // avoid confusing cache behaviours + if (!cached) { + response.setHeader("Cache-Control", "no-cache", false); + } else { + response.setHeader("Cache-Control", "max-age=3600", false); + } + + // send HTML to test allowed/blocked behaviours + response.setHeader("Content-Type", "text/html", false); + + // set signature header and key for Content-Signature header + /* By default a good content-signature header is returned. Any broken return + * value has to be indicated in the url. + */ + let csHeader = ""; + let x5uString = goodX5UString; + let signature = goodSignature; + let file = goodFile; + if (x5uType == "bad") { + x5uString = badX5UString; + } else if (x5uType == "http") { + x5uString = httpX5UString; + } + if (signatureType == "bad") { + signature = badSignature; + } else if (signatureType == "broken") { + signature = brokenSignature; + } else if (signatureType == "sri") { + signature = sriSignature; + } else if (signatureType == "bad-csp") { + signature = badCspSignature; + } + if (fileType == "bad") { + file = getFileName(badFile, "CurWorkD"); + } else if (fileType == "sri") { + file = getFileName(sriFile, "CurWorkD"); + } else if (fileType == "bad-csp") { + file = getFileName(badCspFile, "CurWorkD"); + } + + if (headerType == "good") { + // a valid content-signature header + csHeader = "x5u=" + x5uString + ";p384ecdsa=" + + loadFile(getFileName(signature, "CurWorkD")); + } else if (headerType == "error") { + // this content-signature header is missing ; before p384ecdsa + csHeader = "x5u=" + x5uString + "p384ecdsa=" + + loadFile(getFileName(signature, "CurWorkD")); + } else if (headerType == "errorInX5U") { + // this content-signature header is missing the keyid directive + csHeader = "x6u=" + x5uString + ";p384ecdsa=" + + loadFile(getFileName(signature, "CurWorkD")); + } else if (headerType == "errorInSignature") { + // this content-signature header is missing the p384ecdsa directive + csHeader = "x5u=" + x5uString + ";p385ecdsa=" + + loadFile(getFileName(signature, "CurWorkD")); + } + + if (csHeader) { + response.setHeader("Content-Signature", csHeader, false); + } + let result = loadFile(file); + + response.write(result); +} diff --git a/dom/security/test/contentverifier/goodChain.pem b/dom/security/test/contentverifier/goodChain.pem new file mode 100644 index 000000000..d5047cfc2 --- /dev/null +++ b/dom/security/test/contentverifier/goodChain.pem @@ -0,0 +1,51 @@ +-----BEGIN CERTIFICATE----- +MIICUzCCAT2gAwIBAgIUJ1BtYqWRwUsVaZCGPp9eTHIC04QwCwYJKoZIhvcNAQEL +MBExDzANBgNVBAMMBmludC1DQTAiGA8yMDE1MTEyODAwMDAwMFoYDzIwMTgwMjA1 +MDAwMDAwWjAUMRIwEAYDVQQDDAllZS1pbnQtQ0EwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAShaHJDNitcexiJ83kVRhWhxz+0je6GPgIpFdtgjiUt5LcTLajOmOgxU05q +nAwLCcjWOa3oMgbluoE0c6EfozDgXajJbkOD/ieHPalxA74oiM/wAvBa9xof3cyD +dKpuqc6jTjBMMBMGA1UdJQQMMAoGCCsGAQUFBwMDMDUGA1UdEQQuMCyCKnJlbW90 +ZW5ld3RhYi5jb250ZW50LXNpZ25hdHVyZS5tb3ppbGxhLm9yZzALBgkqhkiG9w0B +AQsDggEBALiLck6k50ok9ahVq45P3feY1PeUXcIYZkJd8aPDYM+0kfg5+JyJBykA +mtHWPE1QQjs7VRMfaLfu04E4UJMI2V1AON1qtgR9BQLctW85KFACg2omfiCKwJh0 +5Q8cxBFx9BpNMayqLJwHttB6oluxZFTB8CL/hfpbYpHz1bMEDCVSRP588YBrc8mV +OLqzQK+k3ewwGvfD6SvXmTny37MxqwxdTPFJNnpqzKAsQIvz8Skic9BkA1NFk0Oq +lsKKoiibbOCmwS9XY/laAkBaC3winuhciYAC0ImAopZ4PBCU0AOHGrNbhZXWYQxt +uHBj34FqvIRCqgM06JCEwN0ULgix4kI= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC0TCCAbugAwIBAgIUPcKbBQpKwTzrrlqzM+d3z5DWiNUwCwYJKoZIhvcNAQEL +MA0xCzAJBgNVBAMMAmNhMCIYDzIwMTUxMTI4MDAwMDAwWhgPMjAxODAyMDUwMDAw +MDBaMBExDzANBgNVBAMMBmludC1DQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALqIUahEjhbWQf1utogGNhA9PBPZ6uQ1SrTs9WhXbCR7wcclqODYH72x +nAabbhqG8mvir1p1a2pkcQh6pVqnRYf3HNUknAJ+zUP8HmnQOCApk6sgw0nk27lM +wmtsDu0Vgg/xfq1pGrHTAjqLKkHup3DgDw2N/WYLK7AkkqR9uYhheZCxV5A90jvF +4LhIH6g304hD7ycW2FW3ZlqqfgKQLzp7EIAGJMwcbJetlmFbt+KWEsB1MaMMkd20 +yvf8rR0l0wnvuRcOp2jhs3svIm9p47SKlWEd7ibWJZ2rkQhONsscJAQsvxaLL+Xx +j5kXMbiz/kkj+nJRxDHVA6zaGAo17Y0CAwEAAaMlMCMwDAYDVR0TBAUwAwEB/zAT +BgNVHSUEDDAKBggrBgEFBQcDAzALBgkqhkiG9w0BAQsDggEBADDPjITgz8joxLRW +wpLxELKSgO/KQ6iAXztjMHq9ovT7Fy0fqBnQ1mMVFr+sBXLgtUCM45aip6PjhUXc +zs5Dq5STg+kz7qtmAjEQvOPcyictbgdu/K7+uMhXQhlzhOgyW88Uk5vrAezNTc/e +TvSmWp1FcgVAfaeMN/90nzD1KIHoUt7zqZIz9ub8jXPVzQNZq4vh33smZhmbdTdV +DaHUyef5cR1VTEGB+L1qzUIQqpHmD4UkMNP1nYedWfauiQhRt6Ql3rJSCRuEvsOA +iBTJlwai/EFwfyfHkOV2GNgv+A5wHHEjBtF5c4PCxQEL5Vw+mfZHLsDVqF3279ZY +lQ6jQ9g= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICzTCCAbegAwIBAgIUKRLJoCmk0A6PHrNc8CxFn//4BYcwCwYJKoZIhvcNAQEL +MA0xCzAJBgNVBAMMAmNhMCIYDzIwMTUxMTI4MDAwMDAwWhgPMjAxODAyMDUwMDAw +MDBaMA0xCzAJBgNVBAMMAmNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAuohRqESOFtZB/W62iAY2ED08E9nq5DVKtOz1aFdsJHvBxyWo4NgfvbGcBptu +Gobya+KvWnVramRxCHqlWqdFh/cc1SScAn7NQ/weadA4ICmTqyDDSeTbuUzCa2wO +7RWCD/F+rWkasdMCOosqQe6ncOAPDY39ZgsrsCSSpH25iGF5kLFXkD3SO8XguEgf +qDfTiEPvJxbYVbdmWqp+ApAvOnsQgAYkzBxsl62WYVu34pYSwHUxowyR3bTK9/yt +HSXTCe+5Fw6naOGzey8ib2njtIqVYR3uJtYlnauRCE42yxwkBCy/Fosv5fGPmRcx +uLP+SSP6clHEMdUDrNoYCjXtjQIDAQABoyUwIzAMBgNVHRMEBTADAQH/MBMGA1Ud +JQQMMAoGCCsGAQUFBwMDMAsGCSqGSIb3DQEBCwOCAQEAABgMK6EyVIXTjD5qaxPO +DWz6yREACmAQBcowKWvfhwgi27DPSXyFGDbzTPEo+7RrIcXJkVAhLouGT51fCwTZ +zb6Sgf6ztX7VSppY9AT4utvlZKP1xQ5WhIYsMtdHCHLHIkRjeWyoBEfUx50UXNLK +Snl+A02GKYWiX+TLLg2DPN2s7v/mm8NLMQNgUlL7KakB2FHFyPa8otPpL4llg7UJ +iBTVQ0c3JoiVbwZaY1Z8QinfMXUrTK9egUC4BAcId1dE8glzA5RRlw1fTLWpGApt +hUmbDnl9N2a9NhGX323ypNzIATexafipzWe7bc4u/+bFdrUqnKUoEka73pZBdHdA +FQ== +-----END CERTIFICATE----- diff --git a/dom/security/test/contentverifier/head.js b/dom/security/test/contentverifier/head.js new file mode 100644 index 000000000..d9637d18b --- /dev/null +++ b/dom/security/test/contentverifier/head.js @@ -0,0 +1,210 @@ +/* + * Test Content-Signature for remote about:newtab + * - Bug 1226928 - allow about:newtab to load remote content + * + * This tests content-signature verification on remote about:newtab in the + * following cases (see TESTS, all failed loads display about:blank fallback): + * - good case (signature should verify and correct page is displayed) + * - reload of newtab when the siganture was invalidated after the last correct + * load + * - malformed content-signature header + * - malformed keyid directive + * - malformed p384ecdsa directive + * - wrong signature (this is not a siganture for the delivered document) + * - invalid signature (this is not even a signature) + * - loading a file that doesn't fit the key or signature + * - cache poisoning (load a malicious remote page not in newtab, subsequent + * newtab load has to load the fallback) + */ + +const ABOUT_NEWTAB_URI = "about:newtab"; + +const BASE = "https://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?"; +const URI_GOOD = BASE + "sig=good&x5u=good&file=good&header=good"; + +const INVALIDATE_FILE = BASE + "invalidateFile=yep"; +const VALIDATE_FILE = BASE + "validateFile=yep"; + +const URI_HEADER_BASE = BASE + "sig=good&x5u=good&file=good&header="; +const URI_ERROR_HEADER = URI_HEADER_BASE + "error"; +const URI_KEYERROR_HEADER = URI_HEADER_BASE + "errorInX5U"; +const URI_SIGERROR_HEADER = URI_HEADER_BASE + "errorInSignature"; +const URI_NO_HEADER = URI_HEADER_BASE + "noHeader"; + +const URI_BAD_SIG = BASE + "sig=bad&x5u=good&file=good&header=good"; +const URI_BROKEN_SIG = BASE + "sig=broken&x5u=good&file=good&header=good"; +const URI_BAD_X5U = BASE + "sig=good&x5u=bad&file=good&header=good"; +const URI_HTTP_X5U = BASE + "sig=good&x5u=http&file=good&header=good"; +const URI_BAD_FILE = BASE + "sig=good&x5u=good&file=bad&header=good"; +const URI_BAD_ALL = BASE + "sig=bad&x5u=bad&file=bad&header=bad"; +const URI_BAD_CSP = BASE + "sig=bad-csp&x5u=good&file=bad-csp&header=good"; + +const URI_BAD_FILE_CACHED = BASE + "sig=good&x5u=good&file=bad&header=good&cached=true"; + +const GOOD_ABOUT_STRING = "Just a fully good testpage for Bug 1226928"; +const BAD_ABOUT_STRING = "Just a bad testpage for Bug 1226928"; +const ABOUT_BLANK = "<head></head><body></body>"; + +const URI_CLEANUP = BASE + "cleanup=true"; +const CLEANUP_DONE = "Done"; + +const URI_SRI = BASE + "sig=sri&x5u=good&file=sri&header=good"; +const STYLESHEET_WITHOUT_SRI_BLOCKED = "Stylesheet without SRI blocked"; +const STYLESHEET_WITH_SRI_BLOCKED = "Stylesheet with SRI blocked"; +const STYLESHEET_WITH_SRI_LOADED = "Stylesheet with SRI loaded"; +const SCRIPT_WITHOUT_SRI_BLOCKED = "Script without SRI blocked"; +const SCRIPT_WITH_SRI_BLOCKED = "Script with SRI blocked"; +const SCRIPT_WITH_SRI_LOADED = "Script with SRI loaded"; + +const CSP_TEST_SUCCESS_STRING = "CSP violation test succeeded."; + +// Needs to sync with pref "security.signed_content.CSP.default". +const SIGNED_CONTENT_CSP = `{"csp-policies":[{"report-only":false,"script-src":["https://example.com","'unsafe-inline'"],"style-src":["https://example.com"]}]}`; + +var browser = null; +var aboutNewTabService = Cc["@mozilla.org/browser/aboutnewtab-service;1"] + .getService(Ci.nsIAboutNewTabService); + +function pushPrefs(...aPrefs) { + return new Promise((resolve) => { + SpecialPowers.pushPrefEnv({"set": aPrefs}, resolve); + }); +} + +/* + * run tests with input from TESTS + */ +function doTest(aExpectedStrings, reload, aUrl, aNewTabPref) { + // set about:newtab location for this test if it's a newtab test + if (aNewTabPref) { + aboutNewTabService.newTabURL = aNewTabPref; + } + + // set prefs + yield pushPrefs( + ["browser.newtabpage.remote.content-signing-test", true], + ["browser.newtabpage.remote", true], + ["security.content.signature.root_hash", + "CC:BE:04:87:74:B2:98:24:4A:C6:7A:71:BC:6F:DB:D6:C0:48:17:29:57:51:96:47:38:CC:24:C8:E4:F9:DD:CB"]); + + if (aNewTabPref === URI_BAD_CSP) { + // Use stricter CSP to test CSP violation. + yield pushPrefs(["security.signed_content.CSP.default", "script-src 'self'; style-src 'self'"]); + } else { + // Use weaker CSP to test normal content. + yield pushPrefs(["security.signed_content.CSP.default", "script-src 'self' 'unsafe-inline'; style-src 'self'"]); + } + + // start the test + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: aUrl, + }, + function * (browser) { + // check if everything's set correct for testing + ok(Services.prefs.getBoolPref( + "browser.newtabpage.remote.content-signing-test"), + "sanity check: remote newtab signing test should be used"); + ok(Services.prefs.getBoolPref("browser.newtabpage.remote"), + "sanity check: remote newtab should be used"); + // we only check this if we really do a newtab test + if (aNewTabPref) { + ok(aboutNewTabService.overridden, + "sanity check: default URL for about:newtab should be overriden"); + is(aboutNewTabService.newTabURL, aNewTabPref, + "sanity check: default URL for about:newtab should return the new URL"); + } + + // Every valid remote newtab page must have built-in CSP. + let shouldHaveCSP = ((aUrl === ABOUT_NEWTAB_URI) && + (aNewTabPref === URI_GOOD || aNewTabPref === URI_SRI)); + + if (shouldHaveCSP) { + is(browser.contentDocument.nodePrincipal.cspJSON, SIGNED_CONTENT_CSP, + "Valid remote newtab page must have built-in CSP."); + } + + yield ContentTask.spawn( + browser, aExpectedStrings, function * (aExpectedStrings) { + for (let expectedString of aExpectedStrings) { + ok(content.document.documentElement.innerHTML.includes(expectedString), + "Expect the following value in the result\n" + expectedString + + "\nand got " + content.document.documentElement.innerHTML); + } + }); + + // for good test cases we check if a reload fails if the remote page + // changed from valid to invalid in the meantime + if (reload) { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: INVALIDATE_FILE, + }, + function * (browser2) { + yield ContentTask.spawn(browser2, null, function * () { + ok(content.document.documentElement.innerHTML.includes("Done"), + "Expect the following value in the result\n" + "Done" + + "\nand got " + content.document.documentElement.innerHTML); + }); + } + ); + + browser.reload(); + yield BrowserTestUtils.browserLoaded(browser); + + let expectedStrings = [ABOUT_BLANK]; + if (aNewTabPref == URI_SRI) { + expectedStrings = [ + STYLESHEET_WITHOUT_SRI_BLOCKED, + STYLESHEET_WITH_SRI_BLOCKED, + SCRIPT_WITHOUT_SRI_BLOCKED, + SCRIPT_WITH_SRI_BLOCKED + ]; + } + yield ContentTask.spawn(browser, expectedStrings, + function * (expectedStrings) { + for (let expectedString of expectedStrings) { + ok(content.document.documentElement.innerHTML.includes(expectedString), + "Expect the following value in the result\n" + expectedString + + "\nand got " + content.document.documentElement.innerHTML); + } + } + ); + + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: VALIDATE_FILE, + }, + function * (browser2) { + yield ContentTask.spawn(browser2, null, function * () { + ok(content.document.documentElement.innerHTML.includes("Done"), + "Expect the following value in the result\n" + "Done" + + "\nand got " + content.document.documentElement.innerHTML); + }); + } + ); + } + } + ); +} + +function runTests() { + // run tests from TESTS + for (let i = 0; i < TESTS.length; i++) { + let testCase = TESTS[i]; + let url = "", aNewTabPref = ""; + let reload = false; + var aExpectedStrings = testCase.testStrings; + if (testCase.aboutURI) { + url = ABOUT_NEWTAB_URI; + aNewTabPref = testCase.aboutURI; + if (aNewTabPref == URI_GOOD || aNewTabPref == URI_SRI) { + reload = true; + } + } else { + url = testCase.url; + } + + yield doTest(aExpectedStrings, reload, url, aNewTabPref); + } +} diff --git a/dom/security/test/contentverifier/script.js b/dom/security/test/contentverifier/script.js new file mode 100644 index 000000000..8fd8f96b2 --- /dev/null +++ b/dom/security/test/contentverifier/script.js @@ -0,0 +1 @@ +var load=true; diff --git a/dom/security/test/contentverifier/signature.der b/dom/security/test/contentverifier/signature.der Binary files differnew file mode 100644 index 000000000..011b94142 --- /dev/null +++ b/dom/security/test/contentverifier/signature.der diff --git a/dom/security/test/contentverifier/sk.pem b/dom/security/test/contentverifier/sk.pem new file mode 100644 index 000000000..2ed514b9f --- /dev/null +++ b/dom/security/test/contentverifier/sk.pem @@ -0,0 +1,9 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAIg== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDAzX2TrGOr0WE92AbAl+nqnpqh25pKCLYNMTV2hJHztrkVPWOp8w0mh +scIodK8RMpagBwYFK4EEACKhZANiAATiTcWYbt0Wg63dO7OXvpptNG0ryxv+v+Js +JJ5Upr3pFus5fZyKxzP9NPzB+oFhL/xw3jMx7X5/vBGaQ2sJSiNlHVkqZgzYF6JQ +4yUyiqTY7v67CyfUPA1BJg/nxOS9m3o= +-----END EC PRIVATE KEY----- diff --git a/dom/security/test/contentverifier/style.css b/dom/security/test/contentverifier/style.css new file mode 100644 index 000000000..c7ab9ecff --- /dev/null +++ b/dom/security/test/contentverifier/style.css @@ -0,0 +1,3 @@ +#red-text { + color: red; +} diff --git a/dom/security/test/cors/file_CrossSiteXHR_cache_server.sjs b/dom/security/test/cors/file_CrossSiteXHR_cache_server.sjs new file mode 100644 index 000000000..8ee4ddbf5 --- /dev/null +++ b/dom/security/test/cors/file_CrossSiteXHR_cache_server.sjs @@ -0,0 +1,49 @@ +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + if ("setState" in query) { + setState("test/dom/security/test_CrossSiteXHR_cache:secData", + query.setState); + + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + response.write("hi"); + + return; + } + + var isPreflight = request.method == "OPTIONS"; + + // Send response + + secData = + eval(getState("test/dom/security/test_CrossSiteXHR_cache:secData")); + + if (secData.allowOrigin) + response.setHeader("Access-Control-Allow-Origin", secData.allowOrigin); + + if (secData.withCred) + response.setHeader("Access-Control-Allow-Credentials", "true"); + + if (isPreflight) { + if (secData.allowHeaders) + response.setHeader("Access-Control-Allow-Headers", secData.allowHeaders); + + if (secData.allowMethods) + response.setHeader("Access-Control-Allow-Methods", secData.allowMethods); + + if (secData.cacheTime) + response.setHeader("Access-Control-Max-Age", secData.cacheTime.toString()); + + return; + } + + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "application/xml", false); + response.write("<res>hello pass</res>\n"); +} diff --git a/dom/security/test/cors/file_CrossSiteXHR_inner.html b/dom/security/test/cors/file_CrossSiteXHR_inner.html new file mode 100644 index 000000000..9268f0ed7 --- /dev/null +++ b/dom/security/test/cors/file_CrossSiteXHR_inner.html @@ -0,0 +1,121 @@ +<!DOCTYPE HTML> +<!-- + NOTE! The content of this file is duplicated in file_CrossSiteXHR_inner.jar + and file_CrossSiteXHR_inner_data.sjs + Please update those files if you update this one. +--> + +<html> +<head> +<script> +function trimString(stringValue) { + return stringValue.replace(/^\s+|\s+$/g, ''); +}; + +window.addEventListener("message", function(e) { + + sendData = null; + + req = eval(e.data); + var res = { + didFail: false, + events: [], + progressEvents: 0, + status: 0, + responseText: "", + statusText: "", + responseXML: null, + sendThrew: false + }; + + var xhr = new XMLHttpRequest(); + for (type of ["load", "abort", "error", "loadstart", "loadend"]) { + xhr.addEventListener(type, function(e) { + res.events.push(e.type); + }, false); + } + xhr.addEventListener("readystatechange", function(e) { + res.events.push("rs" + xhr.readyState); + }, false); + xhr.addEventListener("progress", function(e) { + res.progressEvents++; + }, false); + if (req.uploadProgress) { + xhr.upload.addEventListener(req.uploadProgress, function(e) { + res.progressEvents++; + }, false); + } + xhr.onerror = function(e) { + res.didFail = true; + }; + xhr.onloadend = function (event) { + res.status = xhr.status; + try { + res.statusText = xhr.statusText; + } catch (e) { + delete(res.statusText); + } + res.responseXML = xhr.responseXML ? + (new XMLSerializer()).serializeToString(xhr.responseXML) : + null; + res.responseText = xhr.responseText; + + res.responseHeaders = {}; + for (responseHeader in req.responseHeaders) { + res.responseHeaders[responseHeader] = + xhr.getResponseHeader(responseHeader); + } + res.allResponseHeaders = {}; + var splitHeaders = xhr.getAllResponseHeaders().split("\r\n"); + for (var i = 0; i < splitHeaders.length; i++) { + var headerValuePair = splitHeaders[i].split(":"); + if(headerValuePair[1] != null) { + var headerName = trimString(headerValuePair[0]); + var headerValue = trimString(headerValuePair[1]); + res.allResponseHeaders[headerName] = headerValue; + } + } + post(e, res); + } + + if (req.withCred) + xhr.withCredentials = true; + if (req.body) + sendData = req.body; + + res.events.push("opening"); + // Allow passign in falsy usernames/passwords so we can test them + try { + xhr.open(req.method, req.url, true, + ("username" in req) ? req.username : "", + ("password" in req) ? req.password : "aa"); + } catch (ex) { + res.didFail = true; + post(e, res); + } + + for (header in req.headers) { + xhr.setRequestHeader(header, req.headers[header]); + } + + res.events.push("sending"); + try { + xhr.send(sendData); + } catch (ex) { + res.didFail = true; + res.sendThrew = true; + post(e, res); + } + +}, false); + +function post(e, res) { + e.source.postMessage(res.toSource(), "http://mochi.test:8888"); +} + +</script> +</head> +<body> +Inner page +</body> +</html> diff --git a/dom/security/test/cors/file_CrossSiteXHR_inner.jar b/dom/security/test/cors/file_CrossSiteXHR_inner.jar Binary files differnew file mode 100644 index 000000000..bdb0eb440 --- /dev/null +++ b/dom/security/test/cors/file_CrossSiteXHR_inner.jar diff --git a/dom/security/test/cors/file_CrossSiteXHR_inner_data.sjs b/dom/security/test/cors/file_CrossSiteXHR_inner_data.sjs new file mode 100644 index 000000000..2908921e2 --- /dev/null +++ b/dom/security/test/cors/file_CrossSiteXHR_inner_data.sjs @@ -0,0 +1,103 @@ +var data = '<!DOCTYPE HTML>\n\ +<html>\n\ +<head>\n\ +<script>\n\ +window.addEventListener("message", function(e) {\n\ +\n\ + sendData = null;\n\ +\n\ + req = eval(e.data);\n\ + var res = {\n\ + didFail: false,\n\ + events: [],\n\ + progressEvents: 0\n\ + };\n\ + \n\ + var xhr = new XMLHttpRequest();\n\ + for (type of ["load", "abort", "error", "loadstart", "loadend"]) {\n\ + xhr.addEventListener(type, function(e) {\n\ + res.events.push(e.type);\n\ + }, false);\n\ + }\n\ + xhr.addEventListener("readystatechange", function(e) {\n\ + res.events.push("rs" + xhr.readyState);\n\ + }, false);\n\ + xhr.addEventListener("progress", function(e) {\n\ + res.progressEvents++;\n\ + }, false);\n\ + if (req.uploadProgress) {\n\ + xhr.upload.addEventListener(req.uploadProgress, function(e) {\n\ + res.progressEvents++;\n\ + }, false);\n\ + }\n\ + xhr.onerror = function(e) {\n\ + res.didFail = true;\n\ + };\n\ + xhr.onloadend = function (event) {\n\ + res.status = xhr.status;\n\ + try {\n\ + res.statusText = xhr.statusText;\n\ + } catch (e) {\n\ + delete(res.statusText);\n\ + }\n\ + res.responseXML = xhr.responseXML ?\n\ + (new XMLSerializer()).serializeToString(xhr.responseXML) :\n\ + null;\n\ + res.responseText = xhr.responseText;\n\ +\n\ + res.responseHeaders = {};\n\ + for (responseHeader in req.responseHeaders) {\n\ + res.responseHeaders[responseHeader] =\n\ + xhr.getResponseHeader(responseHeader);\n\ + }\n\ + res.allResponseHeaders = {};\n\ + var splitHeaders = xhr.getAllResponseHeaders().split("\\r\\n");\n\ + for (var i = 0; i < splitHeaders.length; i++) {\n\ + var headerValuePair = splitHeaders[i].split(":");\n\ + if(headerValuePair[1] != null){\n\ + var headerName = trimString(headerValuePair[0]);\n\ + var headerValue = trimString(headerValuePair[1]); \n\ + res.allResponseHeaders[headerName] = headerValue;\n\ + }\n\ + }\n\ + post(e, res);\n\ + }\n\ +\n\ + if (req.withCred)\n\ + xhr.withCredentials = true;\n\ + if (req.body)\n\ + sendData = req.body;\n\ +\n\ + res.events.push("opening");\n\ + xhr.open(req.method, req.url, true);\n\ +\n\ + for (header in req.headers) {\n\ + xhr.setRequestHeader(header, req.headers[header]);\n\ + }\n\ +\n\ + res.events.push("sending");\n\ + xhr.send(sendData);\n\ +\n\ +}, false);\n\ +\n\ +function post(e, res) {\n\ + e.source.postMessage(res.toSource(), "*");\n\ +}\n\ +function trimString(stringValue) {\n\ + return stringValue.replace("/^\s+|\s+$/g","");\n\ +};\n\ +\n\ +</script>\n\ +</head>\n\ +<body>\n\ +Inner page\n\ +</body>\n\ +</html>' + +function handleRequest(request, response) +{ + response.setStatusLine(null, 302, "Follow me"); + response.setHeader("Location", "data:text/html," + escape(data)); + response.setHeader("Content-Type", "text/plain"); + response.write("Follow that guy!"); +} diff --git a/dom/security/test/cors/file_CrossSiteXHR_server.sjs b/dom/security/test/cors/file_CrossSiteXHR_server.sjs new file mode 100644 index 000000000..66d110468 --- /dev/null +++ b/dom/security/test/cors/file_CrossSiteXHR_server.sjs @@ -0,0 +1,179 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + var isPreflight = request.method == "OPTIONS"; + + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var bodyBytes = []; + while ((bodyAvail = bodyStream.available()) > 0) + Array.prototype.push.apply(bodyBytes, bodyStream.readByteArray(bodyAvail)); + + var body = decodeURIComponent( + escape(String.fromCharCode.apply(null, bodyBytes))); + + if (query.hop) { + query.hop = parseInt(query.hop, 10); + hops = eval(query.hops); + var curHop = hops[query.hop - 1]; + query.allowOrigin = curHop.allowOrigin; + query.allowHeaders = curHop.allowHeaders; + query.allowMethods = curHop.allowMethods; + query.allowCred = curHop.allowCred; + query.noAllowPreflight = curHop.noAllowPreflight; + if (curHop.setCookie) { + query.setCookie = unescape(curHop.setCookie); + } + if (curHop.cookie) { + query.cookie = unescape(curHop.cookie); + } + query.noCookie = curHop.noCookie; + } + + // Check that request was correct + + if (!isPreflight && query.body && body != query.body) { + sendHttp500(response, "Wrong body. Expected " + query.body + " got " + + body); + return; + } + + if (!isPreflight && "headers" in query) { + headers = eval(query.headers); + for(headerName in headers) { + // Content-Type is changed if there was a body + if (!(headerName == "Content-Type" && body) && + (!request.hasHeader(headerName) || + request.getHeader(headerName) != headers[headerName])) { + var actual = request.hasHeader(headerName) ? request.getHeader(headerName) + : "<missing header>"; + sendHttp500(response, + "Header " + headerName + " had wrong value. Expected " + + headers[headerName] + " got " + actual); + return; + } + } + } + + if (isPreflight && "requestHeaders" in query && + request.getHeader("Access-Control-Request-Headers") != query.requestHeaders) { + sendHttp500(response, + "Access-Control-Request-Headers had wrong value. Expected " + + query.requestHeaders + " got " + + request.getHeader("Access-Control-Request-Headers")); + return; + } + + if (isPreflight && "requestMethod" in query && + request.getHeader("Access-Control-Request-Method") != query.requestMethod) { + sendHttp500(response, + "Access-Control-Request-Method had wrong value. Expected " + + query.requestMethod + " got " + + request.getHeader("Access-Control-Request-Method")); + return; + } + + if ("origin" in query && request.getHeader("Origin") != query.origin) { + sendHttp500(response, + "Origin had wrong value. Expected " + query.origin + " got " + + request.getHeader("Origin")); + return; + } + + if ("cookie" in query) { + cookies = {}; + request.getHeader("Cookie").split(/ *; */).forEach(function (val) { + var [name, value] = val.split('='); + cookies[name] = unescape(value); + }); + + query.cookie.split(",").forEach(function (val) { + var [name, value] = val.split('='); + if (cookies[name] != value) { + sendHttp500(response, + "Cookie " + name + " had wrong value. Expected " + value + + " got " + cookies[name]); + return; + } + }); + } + + if (query.noCookie && request.hasHeader("Cookie")) { + sendHttp500(response, + "Got cookies when didn't expect to: " + request.getHeader("Cookie")); + return; + } + + // Send response + + if (!isPreflight && query.status) { + response.setStatusLine(null, query.status, query.statusMessage); + } + if (isPreflight && query.preflightStatus) { + response.setStatusLine(null, query.preflightStatus, "preflight status"); + } + + if (query.allowOrigin && (!isPreflight || !query.noAllowPreflight)) + response.setHeader("Access-Control-Allow-Origin", query.allowOrigin); + + if (query.allowCred) + response.setHeader("Access-Control-Allow-Credentials", "true"); + + if (query.setCookie) + response.setHeader("Set-Cookie", query.setCookie + "; path=/"); + + if (isPreflight) { + if (query.allowHeaders) + response.setHeader("Access-Control-Allow-Headers", query.allowHeaders); + + if (query.allowMethods) + response.setHeader("Access-Control-Allow-Methods", query.allowMethods); + } + else { + if (query.responseHeaders) { + let responseHeaders = eval(query.responseHeaders); + for (let responseHeader in responseHeaders) { + response.setHeader(responseHeader, responseHeaders[responseHeader]); + } + } + + if (query.exposeHeaders) + response.setHeader("Access-Control-Expose-Headers", query.exposeHeaders); + } + + if (!isPreflight && query.hop && query.hop < hops.length) { + newURL = hops[query.hop].server + + "/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?" + + "hop=" + (query.hop + 1) + "&hops=" + escape(query.hops); + if ("headers" in query) { + newURL += "&headers=" + escape(query.headers); + } + response.setStatusLine(null, 307, "redirect"); + response.setHeader("Location", newURL); + + return; + } + + // Send response body + if (!isPreflight && request.method != "HEAD") { + response.setHeader("Content-Type", "application/xml", false); + response.write("<res>hello pass</res>\n"); + } + if (isPreflight && "preflightBody" in query) { + response.setHeader("Content-Type", "text/plain", false); + response.write(query.preflightBody); + } +} + +function sendHttp500(response, text) { + response.setStatusLine(null, 500, text); +} diff --git a/dom/security/test/cors/mochitest.ini b/dom/security/test/cors/mochitest.ini new file mode 100644 index 000000000..095b0d09d --- /dev/null +++ b/dom/security/test/cors/mochitest.ini @@ -0,0 +1,11 @@ +[DEFAULT] +support-files = + file_CrossSiteXHR_cache_server.sjs + file_CrossSiteXHR_inner.html + file_CrossSiteXHR_inner.jar + file_CrossSiteXHR_inner_data.sjs + file_CrossSiteXHR_server.sjs + +[test_CrossSiteXHR.html] +[test_CrossSiteXHR_cache.html] +[test_CrossSiteXHR_origin.html] diff --git a/dom/security/test/cors/test_CrossSiteXHR.html b/dom/security/test/cors/test_CrossSiteXHR.html new file mode 100644 index 000000000..b3cda3b87 --- /dev/null +++ b/dom/security/test/cors/test_CrossSiteXHR.html @@ -0,0 +1,1461 @@ +<!DOCTYPE HTML> +<html> +<head> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8"> + <title>Test for Cross Site XMLHttpRequest</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="initTest()"> +<p id="display"> +<iframe id=loader></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.8"> + +const runPreflightTests = 1; +const runCookieTests = 1; +const runRedirectTests = 1; + +var gen; + +function initTest() { + SimpleTest.waitForExplicitFinish(); + // Allow all cookies, then do the actual test initialization + SpecialPowers.pushPrefEnv({"set": [["network.cookie.cookieBehavior", 0]]}, initTestCallback); +} + +function initTestCallback() { + window.addEventListener("message", function(e) { + gen.send(e.data); + }, false); + + gen = runTest(); + + gen.next() +} + +function runTest() { + var loader = document.getElementById('loader'); + var loaderWindow = loader.contentWindow; + loader.onload = function () { gen.next() }; + + // Test preflight-less requests + basePath = "/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?" + baseURL = "http://mochi.test:8888" + basePath; + + // Test preflighted requests + loader.src = "http://example.org/tests/dom/security/test/cors/file_CrossSiteXHR_inner.html"; + origin = "http://example.org"; + yield undefined; + + tests = [// Plain request + { pass: 1, + method: "GET", + noAllowPreflight: 1, + }, + + // undefined username + { pass: 1, + method: "GET", + noAllowPreflight: 1, + username: undefined + }, + + // undefined username and password + { pass: 1, + method: "GET", + noAllowPreflight: 1, + username: undefined, + password: undefined + }, + + // nonempty username + { pass: 0, + method: "GET", + noAllowPreflight: 1, + username: "user", + }, + + // nonempty password + // XXXbz this passes for now, because we ignore passwords + // without usernames in most cases. + { pass: 1, + method: "GET", + noAllowPreflight: 1, + password: "password", + }, + + // Default allowed headers + { pass: 1, + method: "GET", + headers: { "Content-Type": "text/plain", + "Accept": "foo/bar", + "Accept-Language": "sv-SE" }, + noAllowPreflight: 1, + }, + { pass: 0, + method: "GET", + headers: { "Content-Type": "foo/bar", + "Accept": "foo/bar", + "Accept-Language": "sv-SE" }, + noAllowPreflight: 1, + }, + { pass: 0, + method: "GET", + headers: { "Content-Type": "foo/bar, text/plain" }, + noAllowPreflight: 1, + }, + { pass: 0, + method: "GET", + headers: { "Content-Type": "foo/bar, text/plain, garbage" }, + noAllowPreflight: 1, + }, + + // Custom headers + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "X-My-Header", + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue", + "long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header": "secondValue" }, + allowHeaders: "x-my-header, long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header", + }, + { pass: 1, + method: "GET", + headers: { "x-my%-header": "myValue" }, + allowHeaders: "x-my%-header", + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "" }, + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "", + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "y-my-header", + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header y-my-header", + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header, y-my-header z", + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header, y-my-he(ader", + }, + { pass: 0, + method: "GET", + headers: { "myheader": "" }, + allowMethods: "myheader", + }, + { pass: 1, + method: "GET", + headers: { "User-Agent": "myValue" }, + allowHeaders: "User-Agent", + }, + { pass: 0, + method: "GET", + headers: { "User-Agent": "myValue" }, + }, + + // Multiple custom headers + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue", + "second-header": "secondValue", + "third-header": "thirdValue" }, + allowHeaders: "x-my-header, second-header, third-header", + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue", + "second-header": "secondValue", + "third-header": "thirdValue" }, + allowHeaders: "x-my-header,second-header,third-header", + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue", + "second-header": "secondValue", + "third-header": "thirdValue" }, + allowHeaders: "x-my-header ,second-header ,third-header", + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue", + "second-header": "secondValue", + "third-header": "thirdValue" }, + allowHeaders: "x-my-header , second-header , third-header", + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue", + "second-header": "secondValue" }, + allowHeaders: ", x-my-header, , ,, second-header, , ", + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue", + "second-header": "secondValue" }, + allowHeaders: "x-my-header, second-header, unused-header", + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "myValue", + "y-my-header": "secondValue" }, + allowHeaders: "x-my-header", + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "", + "y-my-header": "" }, + allowHeaders: "x-my-header", + }, + + // HEAD requests + { pass: 1, + method: "HEAD", + noAllowPreflight: 1, + }, + + // HEAD with safe headers + { pass: 1, + method: "HEAD", + headers: { "Content-Type": "text/plain", + "Accept": "foo/bar", + "Accept-Language": "sv-SE" }, + noAllowPreflight: 1, + }, + { pass: 0, + method: "HEAD", + headers: { "Content-Type": "foo/bar", + "Accept": "foo/bar", + "Accept-Language": "sv-SE" }, + noAllowPreflight: 1, + }, + { pass: 0, + method: "HEAD", + headers: { "Content-Type": "foo/bar, text/plain" }, + noAllowPreflight: 1, + }, + { pass: 0, + method: "HEAD", + headers: { "Content-Type": "foo/bar, text/plain, garbage" }, + noAllowPreflight: 1, + }, + + // HEAD with custom headers + { pass: 1, + method: "HEAD", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { pass: 0, + method: "HEAD", + headers: { "x-my-header": "myValue" }, + }, + { pass: 0, + method: "HEAD", + headers: { "x-my-header": "myValue" }, + allowHeaders: "", + }, + { pass: 0, + method: "HEAD", + headers: { "x-my-header": "myValue" }, + allowHeaders: "y-my-header", + }, + { pass: 0, + method: "HEAD", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header y-my-header", + }, + + // POST tests + { pass: 1, + method: "POST", + body: "hi there", + noAllowPreflight: 1, + }, + { pass: 1, + method: "POST", + }, + { pass: 1, + method: "POST", + noAllowPreflight: 1, + }, + + // POST with standard headers + { pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain" }, + noAllowPreflight: 1, + }, + { pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "multipart/form-data" }, + noAllowPreflight: 1, + }, + { pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + noAllowPreflight: 1, + }, + { pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar" }, + }, + { pass: 0, + method: "POST", + headers: { "Content-Type": "foo/bar" }, + }, + { pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", + "Accept": "foo/bar", + "Accept-Language": "sv-SE" }, + noAllowPreflight: 1, + }, + { pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar, text/plain" }, + noAllowPreflight: 1, + }, + { pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar, text/plain, garbage" }, + noAllowPreflight: 1, + }, + + // POST with custom headers + { pass: 1, + method: "POST", + body: "hi there", + headers: { "Accept": "foo/bar", + "Accept-Language": "sv-SE", + "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { pass: 1, + method: "POST", + headers: { "Content-Type": "text/plain", + "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", + "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar", + "x-my-header": "myValue" }, + allowHeaders: "x-my-header, content-type", + }, + { pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar" }, + noAllowPreflight: 1, + }, + { pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar", + "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { pass: 1, + method: "POST", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { pass: 1, + method: "POST", + body: "hi there", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header, $_%", + }, + + // Other methods + { pass: 1, + method: "DELETE", + allowMethods: "DELETE", + }, + { pass: 0, + method: "DELETE", + allowHeaders: "DELETE", + }, + { pass: 0, + method: "DELETE", + }, + { pass: 0, + method: "DELETE", + allowMethods: "", + }, + { pass: 1, + method: "DELETE", + allowMethods: "POST, PUT, DELETE", + }, + { pass: 1, + method: "DELETE", + allowMethods: "POST, DELETE, PUT", + }, + { pass: 1, + method: "DELETE", + allowMethods: "DELETE, POST, PUT", + }, + { pass: 1, + method: "DELETE", + allowMethods: "POST ,PUT ,DELETE", + }, + { pass: 1, + method: "DELETE", + allowMethods: "POST,PUT,DELETE", + }, + { pass: 1, + method: "DELETE", + allowMethods: "POST , PUT , DELETE", + }, + { pass: 1, + method: "DELETE", + allowMethods: " ,, PUT ,, , , DELETE , ,", + }, + { pass: 0, + method: "DELETE", + allowMethods: "PUT", + }, + { pass: 0, + method: "DELETE", + allowMethods: "DELETEZ", + }, + { pass: 0, + method: "DELETE", + allowMethods: "DELETE PUT", + }, + { pass: 0, + method: "DELETE", + allowMethods: "DELETE, PUT Z", + }, + { pass: 0, + method: "DELETE", + allowMethods: "DELETE, PU(T", + }, + { pass: 0, + method: "DELETE", + allowMethods: "PUT DELETE", + }, + { pass: 0, + method: "DELETE", + allowMethods: "PUT Z, DELETE", + }, + { pass: 0, + method: "DELETE", + allowMethods: "PU(T, DELETE", + }, + { pass: 0, + method: "MYMETHOD", + allowMethods: "myMethod", + }, + { pass: 0, + method: "PUT", + allowMethods: "put", + }, + + // Progress events + { pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain" }, + uploadProgress: "progress", + }, + { pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain" }, + uploadProgress: "progress", + noAllowPreflight: 1, + }, + + // Status messages + { pass: 1, + method: "GET", + noAllowPreflight: 1, + status: 404, + statusMessage: "nothin' here", + }, + { pass: 1, + method: "GET", + noAllowPreflight: 1, + status: 401, + statusMessage: "no can do", + }, + { pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar" }, + allowHeaders: "content-type", + status: 500, + statusMessage: "server boo", + }, + { pass: 1, + method: "GET", + noAllowPreflight: 1, + status: 200, + statusMessage: "Yes!!", + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "header value" }, + allowHeaders: "x-my-header", + preflightStatus: 400 + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "header value" }, + allowHeaders: "x-my-header", + preflightStatus: 200 + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "header value" }, + allowHeaders: "x-my-header", + preflightStatus: 204 + }, + + // exposed headers + { pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "x-my-header", + expectedResponseHeaders: ["x-my-header"], + }, + { pass: 0, + method: "GET", + origin: "http://invalid", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "x-my-header", + expectedResponseHeaders: [], + }, + { pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + expectedResponseHeaders: [], + }, + { pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "x-my-header y", + expectedResponseHeaders: [], + }, + { pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "y x-my-header", + expectedResponseHeaders: [], + }, + { pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "x-my-header, y-my-header z", + expectedResponseHeaders: [], + }, + { pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "x-my-header, y-my-hea(er", + expectedResponseHeaders: [], + }, + { pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header", + "y-my-header": "y header" }, + exposeHeaders: " , ,,y-my-header,z-my-header, ", + expectedResponseHeaders: ["y-my-header"], + }, + { pass: 1, + method: "GET", + responseHeaders: { "Cache-Control": "cacheControl header", + "Content-Language": "contentLanguage header", + "Expires":"expires header", + "Last-Modified":"lastModified header", + "Pragma":"pragma header", + "Unexpected":"unexpected header" }, + expectedResponseHeaders: ["Cache-Control","Content-Language","Content-Type","Expires","Last-Modified","Pragma"], + }, + // Check that sending a body in the OPTIONS response works + { pass: 1, + method: "DELETE", + allowMethods: "DELETE", + preflightBody: "I'm a preflight response body", + }, + ]; + + if (!runPreflightTests) { + tests = []; + } + + for (test of tests) { + var req = { + url: baseURL + "allowOrigin=" + escape(test.origin || origin), + method: test.method, + headers: test.headers, + uploadProgress: test.uploadProgress, + body: test.body, + responseHeaders: test.responseHeaders, + }; + + if (test.pass) { + req.url += "&origin=" + escape(origin) + + "&requestMethod=" + test.method; + } + + if ("username" in test) { + req.username = test.username; + } + + if ("password" in test) { + req.password = test.password; + } + + if (test.noAllowPreflight) + req.url += "&noAllowPreflight"; + + if (test.pass && "headers" in test) { + function isUnsafeHeader(name) { + lName = name.toLowerCase(); + return lName != "accept" && + lName != "accept-language" && + (lName != "content-type" || + ["text/plain", + "multipart/form-data", + "application/x-www-form-urlencoded"] + .indexOf(test.headers[name].toLowerCase()) == -1); + } + req.url += "&headers=" + escape(test.headers.toSource()); + reqHeaders = + escape(Object.keys(test.headers) + .filter(isUnsafeHeader) + .map(String.toLowerCase) + .sort() + .join(",")); + req.url += reqHeaders ? "&requestHeaders=" + reqHeaders : ""; + } + if ("allowHeaders" in test) + req.url += "&allowHeaders=" + escape(test.allowHeaders); + if ("allowMethods" in test) + req.url += "&allowMethods=" + escape(test.allowMethods); + if (test.body) + req.url += "&body=" + escape(test.body); + if (test.status) { + req.url += "&status=" + test.status; + req.url += "&statusMessage=" + escape(test.statusMessage); + } + if (test.preflightStatus) + req.url += "&preflightStatus=" + test.preflightStatus; + if (test.responseHeaders) + req.url += "&responseHeaders=" + escape(test.responseHeaders.toSource()); + if (test.exposeHeaders) + req.url += "&exposeHeaders=" + escape(test.exposeHeaders); + if (test.preflightBody) + req.url += "&preflightBody=" + escape(test.preflightBody); + + loaderWindow.postMessage(req.toSource(), origin); + res = eval(yield); + + if (test.pass) { + is(res.didFail, false, + "shouldn't have failed in test for " + test.toSource()); + if (test.status) { + is(res.status, test.status, "wrong status in test for " + test.toSource()); + is(res.statusText, test.statusMessage, "wrong status text for " + test.toSource()); + } + else { + is(res.status, 200, "wrong status in test for " + test.toSource()); + is(res.statusText, "OK", "wrong status text for " + test.toSource()); + } + if (test.method !== "HEAD") { + is(res.responseXML, "<res>hello pass</res>", + "wrong responseXML in test for " + test.toSource()); + is(res.responseText, "<res>hello pass</res>\n", + "wrong responseText in test for " + test.toSource()); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs2,rs3,rs4,load,loadend", + "wrong responseText in test for " + test.toSource()); + } + else { + is(res.responseXML, null, + "wrong responseXML in test for " + test.toSource()); + is(res.responseText, "", + "wrong responseText in test for " + test.toSource()); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs2,rs4,load,loadend", + "wrong responseText in test for " + test.toSource()); + } + if (test.responseHeaders) { + for (header in test.responseHeaders) { + if (test.expectedResponseHeaders.indexOf(header) == -1) { + is(res.responseHeaders[header], null, + "|xhr.getResponseHeader()|wrong response header (" + header + ") in test for " + + test.toSource()); + is(res.allResponseHeaders[header], undefined, + "|xhr.getAllResponseHeaderss()|wrong response header (" + header + ") in test for " + + test.toSource()); + } + else { + is(res.responseHeaders[header], test.responseHeaders[header], + "|xhr.getResponseHeader()|wrong response header (" + header + ") in test for " + + test.toSource()); + is(res.allResponseHeaders[header], test.responseHeaders[header], + "|xhr.getAllResponseHeaderss()|wrong response header (" + header + ") in test for " + + test.toSource()); + } + } + } + } + else { + is(res.didFail, true, + "should have failed in test for " + test.toSource()); + is(res.status, 0, "wrong status in test for " + test.toSource()); + is(res.statusText, "", "wrong status text for " + test.toSource()); + is(res.responseXML, null, + "wrong responseXML in test for " + test.toSource()); + is(res.responseText, "", + "wrong responseText in test for " + test.toSource()); + if (!res.sendThrew) { + if (test.username) { + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs4,error,loadend", + "wrong events in test for " + test.toSource()); + } else { + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs2,rs4,error,loadend", + "wrong events in test for " + test.toSource()); + } + } + is(res.progressEvents, 0, + "wrong events in test for " + test.toSource()); + if (test.responseHeaders) { + for (header in test.responseHeaders) { + is(res.responseHeaders[header], null, + "wrong response header (" + header + ") in test for " + + test.toSource()); + } + } + } + } + + // Test cookie behavior + tests = [{ pass: 1, + method: "GET", + withCred: 1, + allowCred: 1, + }, + { pass: 0, + method: "GET", + withCred: 1, + allowCred: 0, + }, + { pass: 0, + method: "GET", + withCred: 1, + allowCred: 1, + origin: "*", + }, + { pass: 1, + method: "GET", + withCred: 0, + allowCred: 1, + origin: "*", + }, + { pass: 1, + method: "GET", + setCookie: "a=1", + withCred: 1, + allowCred: 1, + }, + { pass: 1, + method: "GET", + cookie: "a=1", + withCred: 1, + allowCred: 1, + }, + { pass: 1, + method: "GET", + noCookie: 1, + withCred: 0, + allowCred: 1, + }, + { pass: 0, + method: "GET", + noCookie: 1, + withCred: 1, + allowCred: 1, + }, + { pass: 1, + method: "GET", + setCookie: "a=2", + withCred: 0, + allowCred: 1, + }, + { pass: 1, + method: "GET", + cookie: "a=1", + withCred: 1, + allowCred: 1, + }, + { pass: 1, + method: "GET", + setCookie: "a=2", + withCred: 1, + allowCred: 1, + }, + { pass: 1, + method: "GET", + cookie: "a=2", + withCred: 1, + allowCred: 1, + }, + ]; + + if (!runCookieTests) { + tests = []; + } + + for (test of tests) { + req = { + url: baseURL + "allowOrigin=" + escape(test.origin || origin), + method: test.method, + headers: test.headers, + withCred: test.withCred, + }; + + if (test.allowCred) + req.url += "&allowCred"; + + if (test.setCookie) + req.url += "&setCookie=" + escape(test.setCookie); + if (test.cookie) + req.url += "&cookie=" + escape(test.cookie); + if (test.noCookie) + req.url += "&noCookie"; + + if ("allowHeaders" in test) + req.url += "&allowHeaders=" + escape(test.allowHeaders); + if ("allowMethods" in test) + req.url += "&allowMethods=" + escape(test.allowMethods); + + loaderWindow.postMessage(req.toSource(), origin); + + res = eval(yield); + if (test.pass) { + is(res.didFail, false, + "shouldn't have failed in test for " + test.toSource()); + is(res.status, 200, "wrong status in test for " + test.toSource()); + is(res.statusText, "OK", "wrong status text for " + test.toSource()); + is(res.responseXML, "<res>hello pass</res>", + "wrong responseXML in test for " + test.toSource()); + is(res.responseText, "<res>hello pass</res>\n", + "wrong responseText in test for " + test.toSource()); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs2,rs3,rs4,load,loadend", + "wrong responseText in test for " + test.toSource()); + } + else { + is(res.didFail, true, + "should have failed in test for " + test.toSource()); + is(res.status, 0, "wrong status in test for " + test.toSource()); + is(res.statusText, "", "wrong status text for " + test.toSource()); + is(res.responseXML, null, + "wrong responseXML in test for " + test.toSource()); + is(res.responseText, "", + "wrong responseText in test for " + test.toSource()); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs2,rs4,error,loadend", + "wrong events in test for " + test.toSource()); + is(res.progressEvents, 0, + "wrong events in test for " + test.toSource()); + } + } + + // Make sure to clear cookies to avoid affecting other tests + document.cookie = "a=; path=/; expires=Thu, 01-Jan-1970 00:00:01 GMT" + is(document.cookie, "", "No cookies should be left over"); + + + // Test redirects + is(loader.src, "http://example.org/tests/dom/security/test/cors/file_CrossSiteXHR_inner.html"); + is(origin, "http://example.org"); + + tests = [{ pass: 1, + method: "GET", + hops: [{ server: "http://example.com", + allowOrigin: origin + }, + ], + }, + { pass: 0, + method: "GET", + hops: [{ server: "http://example.com", + allowOrigin: origin + }, + { server: "http://example.org", + allowOrigin: origin + }, + ], + }, + { pass: 1, + method: "GET", + hops: [{ server: "http://example.com", + allowOrigin: origin + }, + { server: "http://example.org", + allowOrigin: "*" + }, + ], + }, + { pass: 0, + method: "GET", + hops: [{ server: "http://example.com", + allowOrigin: origin + }, + { server: "http://example.org", + }, + ], + }, + { pass: 1, + method: "GET", + hops: [{ server: "http://example.org", + }, + { server: "http://example.org", + }, + { server: "http://example.com", + allowOrigin: origin + }, + ], + }, + { pass: 0, + method: "GET", + hops: [{ server: "http://example.org", + }, + { server: "http://example.org", + }, + { server: "http://example.com", + allowOrigin: origin + }, + { server: "http://example.org", + }, + ], + }, + { pass: 0, + method: "GET", + hops: [{ server: "http://example.com", + allowOrigin: origin + }, + { server: "http://test2.example.org:8000", + allowOrigin: origin + }, + { server: "http://sub2.xn--lt-uia.example.org", + allowOrigin: origin + }, + { server: "http://sub1.test1.example.org", + allowOrigin: origin + }, + ], + }, + { pass: 0, + method: "GET", + hops: [{ server: "http://example.com", + allowOrigin: origin + }, + { server: "http://test2.example.org:8000", + allowOrigin: origin + }, + { server: "http://sub2.xn--lt-uia.example.org", + allowOrigin: "*" + }, + { server: "http://sub1.test1.example.org", + allowOrigin: "*" + }, + ], + }, + { pass: 1, + method: "GET", + hops: [{ server: "http://example.com", + allowOrigin: origin + }, + { server: "http://test2.example.org:8000", + allowOrigin: "*" + }, + { server: "http://sub2.xn--lt-uia.example.org", + allowOrigin: "*" + }, + { server: "http://sub1.test1.example.org", + allowOrigin: "*" + }, + ], + }, + { pass: 0, + method: "GET", + hops: [{ server: "http://example.com", + allowOrigin: origin + }, + { server: "http://test2.example.org:8000", + allowOrigin: origin + }, + { server: "http://sub2.xn--lt-uia.example.org", + allowOrigin: "x" + }, + { server: "http://sub1.test1.example.org", + allowOrigin: origin + }, + ], + }, + { pass: 0, + method: "GET", + hops: [{ server: "http://example.com", + allowOrigin: origin + }, + { server: "http://test2.example.org:8000", + allowOrigin: origin + }, + { server: "http://sub2.xn--lt-uia.example.org", + allowOrigin: "*" + }, + { server: "http://sub1.test1.example.org", + allowOrigin: origin + }, + ], + }, + { pass: 0, + method: "GET", + hops: [{ server: "http://example.com", + allowOrigin: origin + }, + { server: "http://test2.example.org:8000", + allowOrigin: origin + }, + { server: "http://sub2.xn--lt-uia.example.org", + allowOrigin: "*" + }, + { server: "http://sub1.test1.example.org", + }, + ], + }, + { pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain" }, + hops: [{ server: "http://example.org", + }, + { server: "http://example.com", + allowOrigin: origin, + }, + ], + }, + { pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", + "my-header": "myValue", + }, + hops: [{ server: "http://example.org", + }, + { server: "http://example.com", + allowOrigin: origin, + allowHeaders: "my-header", + }, + ], + }, + { pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", + "my-header": "myValue", + }, + hops: [{ server: "http://example.org", + }, + { server: "http://example.com", + allowOrigin: origin, + allowHeaders: "my-header", + }, + { server: "http://sub1.test1.example.org", + allowOrigin: origin, + allowHeaders: "my-header", + }, + ], + }, + { pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", + "my-header": "myValue", + }, + hops: [{ server: "http://example.org", + }, + { server: "http://example.com", + allowOrigin: origin, + allowHeaders: "my-header", + }, + { server: "http://example.com", + allowOrigin: origin, + allowHeaders: "my-header", + }, + ], + }, + { pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", + "my-header": "myValue", + }, + hops: [{ server: "http://example.org", + }, + { server: "http://example.com", + allowOrigin: origin, + allowHeaders: "my-header", + }, + { server: "http://example.org", + allowOrigin: origin, + allowHeaders: "my-header", + }, + ], + }, + { pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", + "my-header": "myValue", + }, + hops: [{ server: "http://example.org", + }, + { server: "http://example.com", + allowOrigin: origin, + noAllowPreflight: 1, + }, + ], + }, + { pass: 1, + method: "DELETE", + hops: [{ server: "http://example.org", + }, + { server: "http://example.com", + allowOrigin: origin, + allowMethods: "DELETE", + }, + ], + }, + { pass: 0, + method: "DELETE", + hops: [{ server: "http://example.org", + }, + { server: "http://example.com", + allowOrigin: origin, + allowMethods: "DELETE", + }, + { server: "http://sub1.test1.example.org", + allowOrigin: origin, + allowMethods: "DELETE", + }, + ], + }, + { pass: 0, + method: "DELETE", + hops: [{ server: "http://example.org", + }, + { server: "http://example.com", + allowOrigin: origin, + allowMethods: "DELETE", + }, + { server: "http://example.com", + allowOrigin: origin, + allowMethods: "DELETE", + }, + ], + }, + { pass: 0, + method: "DELETE", + hops: [{ server: "http://example.org", + }, + { server: "http://example.com", + allowOrigin: origin, + allowMethods: "DELETE", + }, + { server: "http://example.org", + allowOrigin: origin, + allowMethods: "DELETE", + }, + ], + }, + { pass: 0, + method: "DELETE", + hops: [{ server: "http://example.org", + }, + { server: "http://example.com", + allowOrigin: origin, + allowMethods: "DELETE", + noAllowPreflight: 1, + }, + ], + }, + { pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", + "my-header": "myValue", + }, + hops: [{ server: "http://example.com", + allowOrigin: origin, + }, + { server: "http://sub1.test1.example.org", + allowOrigin: origin, + }, + ], + }, + { pass: 0, + method: "DELETE", + hops: [{ server: "http://example.com", + allowOrigin: origin, + }, + { server: "http://sub1.test1.example.org", + allowOrigin: origin, + }, + ], + }, + { pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", + "my-header": "myValue", + }, + hops: [{ server: "http://example.com", + }, + { server: "http://sub1.test1.example.org", + allowOrigin: origin, + allowHeaders: "my-header", + }, + ], + }, + { pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain" }, + hops: [{ server: "http://example.org", + }, + { server: "http://example.com", + allowOrigin: origin, + }, + ], + }, + { pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", + "my-header": "myValue", + }, + hops: [{ server: "http://example.com", + allowOrigin: origin, + allowHeaders: "my-header", + }, + { server: "http://example.org", + allowOrigin: origin, + allowHeaders: "my-header", + }, + ], + }, + + // test redirects with different credentials settings + { + // Initialize by setting a cookies for same- and cross- origins. + pass: 1, + method: "GET", + hops: [{ server: origin, + setCookie: escape("a=1"), + }, + { server: "http://example.com", + allowOrigin: origin, + allowCred: 1, + setCookie: escape("a=2"), + }, + ], + withCred: 1, + }, + { pass: 1, + method: "GET", + hops: [{ server: origin, + cookie: escape("a=1"), + }, + { server: origin, + cookie: escape("a=1"), + }, + { server: "http://example.com", + allowOrigin: origin, + noCookie: 1, + }, + ], + withCred: 0, + }, + { pass: 1, + method: "GET", + hops: [{ server: origin, + cookie: escape("a=1"), + }, + { server: origin, + cookie: escape("a=1"), + }, + { server: "http://example.com", + allowOrigin: origin, + allowCred: 1, + cookie: escape("a=2"), + }, + ], + withCred: 1, + }, + // expected fail because allow-credentials CORS header is not set + { pass: 0, + method: "GET", + hops: [{ server: origin, + cookie: escape("a=1"), + }, + { server: origin, + cookie: escape("a=1"), + }, + { server: "http://example.com", + allowOrigin: origin, + cookie: escape("a=2"), + }, + ], + withCred: 1, + }, + { pass: 1, + method: "GET", + hops: [{ server: origin, + cookie: escape("a=1"), + }, + { server: origin, + cookie: escape("a=1"), + }, + { server: "http://example.com", + allowOrigin: '*', + noCookie: 1, + }, + ], + withCred: 0, + }, + { pass: 0, + method: "GET", + hops: [{ server: origin, + cookie: escape("a=1"), + }, + { server: origin, + cookie: escape("a=1"), + }, + { server: "http://example.com", + allowOrigin: '*', + allowCred: 1, + cookie: escape("a=2"), + }, + ], + withCred: 1, + }, + ]; + + if (!runRedirectTests) { + tests = []; + } + + for (test of tests) { + req = { + url: test.hops[0].server + basePath + "hop=1&hops=" + + escape(test.hops.toSource()), + method: test.method, + headers: test.headers, + body: test.body, + withCred: test.withCred, + }; + + if (test.pass) { + if (test.body) + req.url += "&body=" + escape(test.body); + } + + loaderWindow.postMessage(req.toSource(), origin); + + res = eval(yield); + if (test.pass) { + is(res.didFail, false, + "shouldn't have failed in test for " + test.toSource()); + is(res.status, 200, "wrong status in test for " + test.toSource()); + is(res.statusText, "OK", "wrong status text for " + test.toSource()); + is(res.responseXML, "<res>hello pass</res>", + "wrong responseXML in test for " + test.toSource()); + is(res.responseText, "<res>hello pass</res>\n", + "wrong responseText in test for " + test.toSource()); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs2,rs3,rs4,load,loadend", + "wrong responseText in test for " + test.toSource()); + } + else { + is(res.didFail, true, + "should have failed in test for " + test.toSource()); + is(res.status, 0, "wrong status in test for " + test.toSource()); + is(res.statusText, "", "wrong status text for " + test.toSource()); + is(res.responseXML, null, + "wrong responseXML in test for " + test.toSource()); + is(res.responseText, "", + "wrong responseText in test for " + test.toSource()); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs2,rs4,error,loadend", + "wrong events in test for " + test.toSource()); + is(res.progressEvents, 0, + "wrong progressevents in test for " + test.toSource()); + } + } + + + SimpleTest.finish(); + + yield undefined; +} + +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/cors/test_CrossSiteXHR_cache.html b/dom/security/test/cors/test_CrossSiteXHR_cache.html new file mode 100644 index 000000000..3252eff4a --- /dev/null +++ b/dom/security/test/cors/test_CrossSiteXHR_cache.html @@ -0,0 +1,587 @@ +<!DOCTYPE HTML> +<html> +<head> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8"> + <title>Test for Cross Site XMLHttpRequest</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="gen.next()"> +<p id="display"> +<iframe id=loader></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.7"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("This test needs to generate artificial pauses, hence it uses timeouts. There is no way around it, unfortunately. :("); + +window.addEventListener("message", function(e) { + gen.send(e.data); +}, false); + +gen = runTest(); + +function runTest() { + var loader = document.getElementById('loader'); + var loaderWindow = loader.contentWindow; + loader.onload = function () { gen.next() }; + + loader.src = "http://example.org/tests/dom/security/test/cors/file_CrossSiteXHR_inner.html"; + origin = "http://example.org"; + yield undefined; + + tests = [{ pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + cacheTime: 3600 + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "myValue", + "y-my-header": "second" }, + }, + { pass: 1, + method: "GET", + headers: { "y-my-header": "hello" }, + allowHeaders: "y-my-header", + }, + { pass: 0, + method: "GET", + headers: { "y-my-header": "hello" }, + }, + { pass: 1, + method: "GET", + headers: { "y-my-header": "hello" }, + allowHeaders: "y-my-header,x-my-header", + cacheTime: 3600, + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue", + "y-my-header": "second" }, + }, + { newTest: "*******" }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + cacheTime: 2 + }, + { pause: 2.1 }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + }, + { newTest: "*******" }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header, y-my-header", + cacheTime: 3600 + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + }, + { pass: 1, + method: "GET", + headers: { "y-my-header": "myValue" }, + }, + { pass: 0, + method: "GET", + headers: { "z-my-header": "myValue" }, + }, + { newTest: "*******" }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + cacheTime: "\t 3600 \t ", + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + }, + { newTest: "*******" }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + cacheTime: "3600 3", + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + }, + { newTest: "*******" }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + cacheTime: "asdf", + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + }, + { newTest: "*******" }, + { pass: 1, + method: "GET", + headers: { "first-header": "myValue" }, + allowHeaders: "first-header", + cacheTime: 2, + }, + { pass: 1, + method: "GET", + headers: { "second-header": "myValue" }, + allowHeaders: "second-header", + cacheTime: 3600, + }, + { pass: 1, + method: "GET", + headers: { "third-header": "myValue" }, + allowHeaders: "third-header", + cacheTime: 2, + }, + { pause: 2.1 }, + { pass: 1, + method: "GET", + headers: { "second-header": "myValue" }, + }, + { pass: 0, + method: "GET", + headers: { "first-header": "myValue" }, + }, + { newTest: "*******" }, + { pass: 1, + method: "GET", + headers: { "first-header": "myValue" }, + allowHeaders: "first-header", + cacheTime: 2, + }, + { pass: 1, + method: "GET", + headers: { "second-header": "myValue" }, + allowHeaders: "second-header", + cacheTime: 3600, + }, + { pass: 1, + method: "GET", + headers: { "third-header": "myValue" }, + allowHeaders: "third-header", + cacheTime: 2, + }, + { pause: 2.1 }, + { pass: 1, + method: "GET", + headers: { "second-header": "myValue" }, + }, + { pass: 0, + method: "GET", + headers: { "third-header": "myValue" }, + }, + { newTest: "*******" }, + { pass: 0, + method: "DELETE", + }, + { pass: 1, + method: "DELETE", + allowMethods: "DELETE", + cacheTime: 3600 + }, + { pass: 1, + method: "DELETE", + }, + { pass: 1, + method: "DELETE", + }, + { pass: 0, + method: "PATCH", + }, + { pass: 1, + method: "PATCH", + allowMethods: "PATCH", + }, + { pass: 0, + method: "PATCH", + }, + { pass: 1, + method: "PATCH", + allowMethods: "PATCH", + cacheTime: 3600, + }, + { pass: 1, + method: "PATCH", + }, + { pass: 0, + method: "DELETE", + }, + { pass: 0, + method: "PUT", + }, + { newTest: "*******" }, + { pass: 0, + method: "DELETE", + }, + { pass: 1, + method: "DELETE", + allowMethods: "DELETE", + cacheTime: 2 + }, + { pause: 2.1 }, + { pass: 0, + method: "DELETE", + }, + { newTest: "*******" }, + { pass: 1, + method: "DELETE", + allowMethods: "DELETE, PUT", + cacheTime: 3600 + }, + { pass: 1, + method: "DELETE", + }, + { pass: 1, + method: "PUT", + }, + { pass: 0, + method: "PATCH", + }, + { newTest: "*******" }, + { pass: 1, + method: "FIRST", + allowMethods: "FIRST", + cacheTime: 2, + }, + { pass: 1, + method: "SECOND", + allowMethods: "SECOND", + cacheTime: 3600, + }, + { pass: 1, + method: "THIRD", + allowMethods: "THIRD", + cacheTime: 2, + }, + { pause: 2.1 }, + { pass: 1, + method: "SECOND", + }, + { pass: 0, + method: "FIRST", + }, + { newTest: "*******" }, + { pass: 1, + method: "FIRST", + allowMethods: "FIRST", + cacheTime: 2, + }, + { pass: 1, + method: "SECOND", + allowMethods: "SECOND", + cacheTime: 3600, + }, + { pass: 1, + method: "THIRD", + allowMethods: "THIRD", + cacheTime: 2, + }, + { pause: 2.1 }, + { pass: 1, + method: "SECOND", + }, + { pass: 0, + method: "THIRD", + }, + { newTest: "*******" }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "x-value" }, + allowHeaders: "x-my-header", + cacheTime: 3600, + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "x-value" } + }, + { pass: 0, + method: "GET", + headers: { "y-my-header": "y-value" } + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "x-value" } + }, + { newTest: "*******" }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "x-value" }, + allowHeaders: "x-my-header", + cacheTime: 3600, + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "x-value" }, + }, + { pass: 0, + method: "PUT", + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "x-value" }, + }, + { newTest: "*******" }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "x-value" }, + allowHeaders: "x-my-header", + cacheTime: 3600, + }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "x-value" }, + }, + { pass: 0, + method: "GET", + noOrigin: 1, + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "x-value" }, + }, + { newTest: "*******" }, + { pass: 1, + method: "DELETE", + allowMethods: "DELETE", + cacheTime: 3600, + }, + { pass: 1, + method: "DELETE" + }, + { pass: 0, + method: "PUT" + }, + { pass: 0, + method: "DELETE" + }, + { newTest: "*******" }, + { pass: 1, + method: "DELETE", + allowMethods: "DELETE", + cacheTime: 3600, + }, + { pass: 1, + method: "DELETE" + }, + { pass: 0, + method: "DELETE", + headers: { "my-header": "value" }, + }, + { pass: 0, + method: "DELETE" + }, + { newTest: "*******" }, + { pass: 1, + method: "DELETE", + allowMethods: "DELETE", + cacheTime: 3600, + }, + { pass: 1, + method: "DELETE" + }, + { pass: 0, + method: "GET", + noOrigin: 1, + }, + { pass: 0, + method: "DELETE" + }, + { newTest: "*******" }, + { pass: 1, + method: "GET", + withCred: true, + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + cacheTime: 3600 + }, + { pass: 1, + method: "GET", + withCred: true, + headers: { "x-my-header": "myValue" }, + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + }, + { newTest: "*******" }, + { pass: 1, + method: "GET", + withCred: true, + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + cacheTime: 3600 + }, + { pass: 1, + method: "GET", + headers: { "y-my-header": "myValue" }, + allowHeaders: "y-my-header", + cacheTime: 2 + }, + { pass: 1, + method: "GET", + headers: { "y-my-header": "myValue" }, + }, + { pass: 1, + method: "GET", + withCred: true, + headers: { "x-my-header": "myValue" }, + }, + { pause: 2.1 }, + { pass: 1, + method: "GET", + withCred: true, + headers: { "x-my-header": "myValue" }, + }, + { pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + }, + { pass: 0, + method: "GET", + headers: { "y-my-header": "myValue" }, + }, + { pass: 0, + method: "GET", + withCred: true, + headers: { "y-my-header": "myValue" }, + }, + { newTest: "*******" }, + { pass: 1, + method: "DELETE", + allowMethods: "DELETE", + cacheTime: 3600 + }, + { pass: 0, + method: "GET", + headers: { "DELETE": "myvalue" }, + }, + { newTest: "*******" }, + { pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + cacheTime: 3600 + }, + { pass: 0, + method: "3600", + headers: { "x-my-header": "myvalue" }, + }, + ]; + + for (let i = 0; i < 110; i++) { + tests.push({ newTest: "*******" }, + { pass: 1, + method: "DELETE", + allowMethods: "DELETE", + cacheTime: 3600, + }); + } + + baseURL = "http://mochi.test:8888/tests/dom/security/test/cors/" + + "file_CrossSiteXHR_cache_server.sjs?"; + setStateURL = baseURL + "setState="; + + var unique = Date.now(); + for (test of tests) { + if (test.newTest) { + unique++; + continue; + } + if (test.pause) { + setTimeout(function() { gen.next() }, test.pause * 1000); + yield undefined; + continue; + } + + req = { + url: baseURL + "c=" + unique, + method: test.method, + headers: test.headers, + withCred: test.withCred, + }; + + sec = { allowOrigin: test.noOrigin ? "" : origin, + allowHeaders: test.allowHeaders, + allowMethods: test.allowMethods, + cacheTime: test.cacheTime, + withCred: test.withCred }; + xhr = new XMLHttpRequest(); + xhr.open("POST", setStateURL + escape(sec.toSource()), true); + xhr.onloadend = function() { gen.next(); } + xhr.send(); + yield undefined; + + loaderWindow.postMessage(req.toSource(), origin); + + res = eval(yield); + + testName = test.toSource() + " (index " + tests.indexOf(test) + ")"; + + if (test.pass) { + is(res.didFail, false, + "shouldn't have failed in test for " + testName); + is(res.status, 200, "wrong status in test for " + testName); + is(res.responseXML, "<res>hello pass</res>", + "wrong responseXML in test for " + testName); + is(res.responseText, "<res>hello pass</res>\n", + "wrong responseText in test for " + testName); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs2,rs3,rs4,load,loadend", + "wrong events in test for " + testName); + } + else { + is(res.didFail, true, + "should have failed in test for " + testName); + is(res.status, 0, "wrong status in test for " + testName); + is(res.responseXML, null, + "wrong responseXML in test for " + testName); + is(res.responseText, "", + "wrong responseText in test for " + testName); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs2,rs4,error,loadend", + "wrong events in test for " + testName); + is(res.progressEvents, 0, + "wrong events in test for " + testName); + } + } + + SimpleTest.finish(); + + yield undefined; +} + +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/cors/test_CrossSiteXHR_origin.html b/dom/security/test/cors/test_CrossSiteXHR_origin.html new file mode 100644 index 000000000..1012ae593 --- /dev/null +++ b/dom/security/test/cors/test_CrossSiteXHR_origin.html @@ -0,0 +1,174 @@ +<!DOCTYPE HTML> +<html> +<head> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8"> + <title>Test for Cross Site XMLHttpRequest</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"> +<iframe id=loader></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript;version=1.8"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(2); + +var origins = + [{ server: 'http://example.org' }, + { server: 'http://example.org:80', + origin: 'http://example.org' + }, + { server: 'http://sub1.test1.example.org' }, + { server: 'http://test2.example.org:8000' }, + { server: 'http://sub1.\xe4lt.example.org:8000', + origin: 'http://sub1.xn--lt-uia.example.org:8000' + }, + { server: 'http://sub2.\xe4lt.example.org', + origin: 'http://sub2.xn--lt-uia.example.org' + }, + { server: 'http://ex\xe4mple.test', + origin: 'http://xn--exmple-cua.test' + }, + { server: 'http://xn--exmple-cua.test' }, + { server: 'http://\u03c0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1.\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae', + origin: 'http://xn--hxajbheg2az3al.xn--jxalpdlp' + }, + { origin: 'http://example.org', + file: 'jar:http://example.org/tests/dom/security/test/cors/file_CrossSiteXHR_inner.jar!/file_CrossSiteXHR_inner.html' + }, + { origin: 'null', + file: 'http://example.org/tests/dom/security/test/cors/file_CrossSiteXHR_inner_data.sjs' + }, + ]; + + //['https://example.com:443'], + //['https://sub1.test1.example.com:443'], + +window.addEventListener("message", function(e) { + gen.send(e.data); +}, false); + +gen = runTest(); + +function runTest() { + var loader = document.getElementById('loader'); + var loaderWindow = loader.contentWindow; + loader.onload = function () { gen.next() }; + + // Test preflight-less requests + basePath = "/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?" + baseURL = "http://mochi.test:8888" + basePath; + + for (originEntry of origins) { + origin = originEntry.origin || originEntry.server; + + loader.src = originEntry.file || + (originEntry.server + "/tests/dom/security/test/cors/file_CrossSiteXHR_inner.html"); + yield undefined; + + var isNullOrigin = origin == "null"; + + port = /:\d+/; + passTests = [ + origin, + "*", + " \t " + origin + "\t \t", + "\t \t* \t ", + ]; + failTests = [ + "", + " ", + port.test(origin) ? origin.replace(port, "") + : origin + ":1234", + port.test(origin) ? origin.replace(port, ":") + : origin + ":", + origin + ".", + origin + "/", + origin + "#", + origin + "?", + origin + "\\", + origin + "%", + origin + "@", + origin + "/hello", + "foo:bar@" + origin, + "* " + origin, + origin + " " + origin, + "allow <" + origin + ">", + "<" + origin + ">", + "<*>", + origin.substr(0, 5) == "https" ? origin.replace("https", "http") + : origin.replace("http", "https"), + origin.replace("://", "://www."), + origin.replace("://", ":// "), + origin.replace(/\/[^.]+\./, "/"), + ]; + + if (isNullOrigin) { + passTests = ["*", "\t \t* \t ", "null"]; + failTests = failTests.filter(function(v) { return v != origin }); + } + + for (allowOrigin of passTests) { + req = { + url: baseURL + + "allowOrigin=" + escape(allowOrigin) + + "&origin=" + escape(origin), + method: "GET", + }; + loaderWindow.postMessage(req.toSource(), isNullOrigin ? "*" : origin); + + res = eval(yield); + is(res.didFail, false, "shouldn't have failed for " + allowOrigin); + is(res.status, 200, "wrong status for " + allowOrigin); + is(res.statusText, "OK", "wrong status text for " + allowOrigin); + is(res.responseXML, + "<res>hello pass</res>", + "wrong responseXML in test for " + allowOrigin); + is(res.responseText, "<res>hello pass</res>\n", + "wrong responseText in test for " + allowOrigin); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs2,rs3,rs4,load,loadend", + "wrong responseText in test for " + allowOrigin); + } + + for (allowOrigin of failTests) { + req = { + url: baseURL + "allowOrigin=" + escape(allowOrigin), + method: "GET", + }; + loaderWindow.postMessage(req.toSource(), isNullOrigin ? "*" : origin); + + res = eval(yield); + is(res.didFail, true, "should have failed for " + allowOrigin); + is(res.responseText, "", "should have no text for " + allowOrigin); + is(res.status, 0, "should have no status for " + allowOrigin); + is(res.statusText, "", "wrong status text for " + allowOrigin); + is(res.responseXML, null, "should have no XML for " + allowOrigin); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs2,rs4,error,loadend", + "wrong events in test for " + allowOrigin); + is(res.progressEvents, 0, + "wrong events in test for " + allowOrigin); + } + } + + SimpleTest.finish(); + + yield undefined; +} + +addLoadEvent(function() { + SpecialPowers.pushPrefEnv({"set": [["network.jar.block-remote-files", false]]}, function() { + gen.next(); + }); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/browser.ini b/dom/security/test/csp/browser.ini new file mode 100644 index 000000000..0846e87fe --- /dev/null +++ b/dom/security/test/csp/browser.ini @@ -0,0 +1,13 @@ +[DEFAULT] +support-files = + !/dom/security/test/csp/file_testserver.sjs + !/dom/security/test/csp/file_web_manifest.html + !/dom/security/test/csp/file_web_manifest.json + !/dom/security/test/csp/file_web_manifest.json^headers^ + !/dom/security/test/csp/file_web_manifest_https.html + !/dom/security/test/csp/file_web_manifest_https.json + !/dom/security/test/csp/file_web_manifest_mixed_content.html + !/dom/security/test/csp/file_web_manifest_remote.html +[browser_test_web_manifest.js] +[browser_test_web_manifest_mixed_content.js] +[browser_manifest-src-override-default-src.js] diff --git a/dom/security/test/csp/browser_manifest-src-override-default-src.js b/dom/security/test/csp/browser_manifest-src-override-default-src.js new file mode 100644 index 000000000..0c4c7b7bc --- /dev/null +++ b/dom/security/test/csp/browser_manifest-src-override-default-src.js @@ -0,0 +1,108 @@ +/* + * Description of the tests: + * Tests check that default-src can be overridden by manifest-src. + */ +/*globals Cu, is, ok*/ +"use strict"; +const { + ManifestObtainer +} = Cu.import("resource://gre/modules/ManifestObtainer.jsm", {}); +const path = "/tests/dom/security/test/csp/"; +const testFile = `${path}file_web_manifest.html`; +const mixedContentFile = `${path}file_web_manifest_mixed_content.html`; +const server = `${path}file_testserver.sjs`; +const defaultURL = new URL(`http://example.org${server}`); +const mixedURL = new URL(`http://mochi.test:8888${server}`); +const tests = [ + // Check interaction with default-src and another origin, + // CSP allows fetching from example.org, so manifest should load. + { + expected: `CSP manifest-src overrides default-src of elsewhere.com`, + get tabURL() { + const url = new URL(defaultURL); + url.searchParams.append("file", testFile); + url.searchParams.append("cors", "*"); + url.searchParams.append("csp", "default-src http://elsewhere.com; manifest-src http://example.org"); + return url.href; + }, + run(manifest) { + is(manifest.name, "loaded", this.expected); + } + }, + // Check interaction with default-src none, + // CSP allows fetching manifest from example.org, so manifest should load. + { + expected: `CSP manifest-src overrides default-src`, + get tabURL() { + const url = new URL(mixedURL); + url.searchParams.append("file", mixedContentFile); + url.searchParams.append("cors", "http://test:80"); + url.searchParams.append("csp", "default-src 'self'; manifest-src http://test:80"); + return url.href; + }, + run(manifest) { + is(manifest.name, "loaded", this.expected); + } + }, +]; + +//jscs:disable +add_task(function* () { + //jscs:enable + const testPromises = tests.map((test) => { + const tabOptions = { + gBrowser, + url: test.tabURL, + skipAnimation: true, + }; + return BrowserTestUtils.withNewTab(tabOptions, (browser) => testObtainingManifest(browser, test)); + }); + yield Promise.all(testPromises); +}); + +function* testObtainingManifest(aBrowser, aTest) { + const expectsBlocked = aTest.expected.includes("block"); + const observer = (expectsBlocked) ? createNetObserver(aTest) : null; + // Expect an exception (from promise rejection) if there a content policy + // that is violated. + try { + const manifest = yield ManifestObtainer.browserObtainManifest(aBrowser); + aTest.run(manifest); + } catch (e) { + const wasBlocked = e.message.includes("NetworkError when attempting to fetch resource"); + ok(wasBlocked,`Expected promise rejection obtaining ${aTest.tabURL}: ${e.message}`); + if (observer) { + yield observer.untilFinished; + } + } +} + +// Helper object used to observe policy violations. It waits 1 seconds +// for a response, and then times out causing its associated test to fail. +function createNetObserver(test) { + let finishedTest; + let success = false; + const finished = new Promise((resolver) => { + finishedTest = resolver; + }); + const timeoutId = setTimeout(() => { + if (!success) { + test.run("This test timed out."); + finishedTest(); + } + }, 1000); + var observer = { + get untilFinished(){ + return finished; + }, + observe(subject, topic) { + SpecialPowers.removeObserver(observer, "csp-on-violate-policy"); + test.run(topic); + finishedTest(); + clearTimeout(timeoutId); + success = true; + }, + }; + SpecialPowers.addObserver(observer, "csp-on-violate-policy", false); + return observer; +} diff --git a/dom/security/test/csp/browser_test_web_manifest.js b/dom/security/test/csp/browser_test_web_manifest.js new file mode 100644 index 000000000..df23770ba --- /dev/null +++ b/dom/security/test/csp/browser_test_web_manifest.js @@ -0,0 +1,224 @@ +/* + * Description of the tests: + * These tests check for conformance to the CSP spec as they relate to Web Manifests. + * + * In particular, the tests check that default-src and manifest-src directives are + * are respected by the ManifestObtainer. + */ +/*globals Cu, is, ok*/ +"use strict"; +const { + ManifestObtainer +} = Cu.import("resource://gre/modules/ManifestObtainer.jsm", {}); +const path = "/tests/dom/security/test/csp/"; +const testFile = `${path}file_web_manifest.html`; +const remoteFile = `${path}file_web_manifest_remote.html`; +const httpsManifest = `${path}file_web_manifest_https.html`; +const server = `${path}file_testserver.sjs`; +const defaultURL = new URL(`http://example.org${server}`); +const secureURL = new URL(`https://example.com:443${server}`); +const tests = [ + // CSP block everything, so trying to load a manifest + // will result in a policy violation. + { + expected: "default-src 'none' blocks fetching manifest.", + get tabURL() { + const url = new URL(defaultURL); + url.searchParams.append("file", testFile); + url.searchParams.append("csp", "default-src 'none'"); + return url.href; + }, + run(topic) { + is(topic, "csp-on-violate-policy", this.expected); + } + }, + // CSP allows fetching only from mochi.test:8888, + // so trying to load a manifest from same origin + // triggers a CSP violation. + { + expected: "default-src mochi.test:8888 blocks manifest fetching.", + get tabURL() { + const url = new URL(defaultURL); + url.searchParams.append("file", testFile); + url.searchParams.append("csp", "default-src mochi.test:8888"); + return url.href; + }, + run(topic) { + is(topic, "csp-on-violate-policy", this.expected); + } + }, + // CSP restricts fetching to 'self', so allowing the manifest + // to load. The name of the manifest is then checked. + { + expected: "CSP default-src 'self' allows fetch of manifest.", + get tabURL() { + const url = new URL(defaultURL); + url.searchParams.append("file", testFile); + url.searchParams.append("csp", "default-src 'self'"); + return url.href; + }, + run(manifest) { + is(manifest.name, "loaded", this.expected); + } + }, + // CSP only allows fetching from mochi.test:8888 and remoteFile + // requests a manifest from that origin, so manifest should load. + { + expected: "CSP default-src mochi.test:8888 allows fetching manifest.", + get tabURL() { + const url = new URL(defaultURL); + url.searchParams.append("file", remoteFile); + url.searchParams.append("csp", "default-src http://mochi.test:8888"); + return url.href; + }, + run(manifest) { + is(manifest.name, "loaded", this.expected); + } + }, + // default-src blocks everything, so any attempt to + // fetch a manifest from another origin will trigger a + // policy violation. + { + expected: "default-src 'none' blocks mochi.test:8888", + get tabURL() { + const url = new URL(defaultURL); + url.searchParams.append("file", remoteFile); + url.searchParams.append("csp", "default-src 'none'"); + return url.href; + }, + run(topic) { + is(topic, "csp-on-violate-policy", this.expected); + } + }, + // CSP allows fetching from self, so manifest should load. + { + expected: "CSP manifest-src allows self", + get tabURL() { + const url = new URL(defaultURL); + url.searchParams.append("file", testFile); + url.searchParams.append("csp", "manifest-src 'self'"); + return url.href; + }, + run(manifest) { + is(manifest.name, "loaded", this.expected); + } + }, + // CSP allows fetching from example.org, so manifest should load. + { + expected: "CSP manifest-src allows http://example.org", + get tabURL() { + const url = new URL(defaultURL); + url.searchParams.append("file", testFile); + url.searchParams.append("csp", "manifest-src http://example.org"); + return url.href; + }, + run(manifest) { + is(manifest.name, "loaded", this.expected); + } + }, { + expected: "CSP manifest-src allows mochi.test:8888", + get tabURL() { + const url = new URL(defaultURL); + url.searchParams.append("file", remoteFile); + url.searchParams.append("cors", "*"); + url.searchParams.append("csp", "default-src *; manifest-src http://mochi.test:8888"); + return url.href; + }, + run(manifest) { + is(manifest.name, "loaded", this.expected); + } + }, + // CSP restricts fetching to mochi.test:8888, but the test + // file is at example.org. Hence, a policy violation is + // triggered. + { + expected: "CSP blocks manifest fetching from example.org.", + get tabURL() { + const url = new URL(defaultURL); + url.searchParams.append("file", testFile); + url.searchParams.append("csp", "manifest-src mochi.test:8888"); + return url.href; + }, + run(topic) { + is(topic, "csp-on-violate-policy", this.expected); + } + }, + // CSP is set to only allow manifest to be loaded from same origin, + // but the remote file attempts to load from a different origin. Thus + // this causes a CSP violation. + { + expected: "CSP manifest-src 'self' blocks cross-origin fetch.", + get tabURL() { + const url = new URL(defaultURL); + url.searchParams.append("file", remoteFile); + url.searchParams.append("csp", "manifest-src 'self'"); + return url.href; + }, + run(topic) { + is(topic, "csp-on-violate-policy", this.expected); + } + }, + // CSP allows fetching over TLS from example.org, so manifest should load. + { + expected: "CSP manifest-src allows example.com over TLS", + get tabURL() { + // secureURL loads https://example.com:443 + // and gets manifest from https://example.org:443 + const url = new URL(secureURL); + url.searchParams.append("file", httpsManifest); + url.searchParams.append("cors", "*"); + url.searchParams.append("csp", "manifest-src https://example.com:443"); + return url.href; + }, + run(manifest) { + is(manifest.name, "loaded", this.expected); + } + }, +]; + +//jscs:disable +add_task(function* () { + //jscs:enable + const testPromises = tests.map((test) => { + const tabOptions = { + gBrowser, + url: test.tabURL, + skipAnimation: true, + }; + return BrowserTestUtils.withNewTab(tabOptions, (browser) => testObtainingManifest(browser, test)); + }); + yield Promise.all(testPromises); +}); + +function* testObtainingManifest(aBrowser, aTest) { + const waitForObserver = waitForNetObserver(aTest); + // Expect an exception (from promise rejection) if there a content policy + // that is violated. + try { + const manifest = yield ManifestObtainer.browserObtainManifest(aBrowser); + aTest.run(manifest); + } catch (e) { + const wasBlocked = e.message.includes("NetworkError when attempting to fetch resource"); + ok(wasBlocked, `Expected promise rejection obtaining ${aTest.tabURL}: ${e.message}`); + } finally { + yield waitForObserver; + } +} + +// Helper object used to observe policy violations when blocking is expected. +function waitForNetObserver(aTest) { + return new Promise((resolve) => { + // We don't need to wait for violation, so just resolve + if (!aTest.expected.includes("block")){ + return resolve(); + } + const observer = { + observe(subject, topic) { + SpecialPowers.removeObserver(observer, "csp-on-violate-policy"); + aTest.run(topic); + resolve(); + }, + }; + SpecialPowers.addObserver(observer, "csp-on-violate-policy", false); + }); +} diff --git a/dom/security/test/csp/browser_test_web_manifest_mixed_content.js b/dom/security/test/csp/browser_test_web_manifest_mixed_content.js new file mode 100644 index 000000000..9238acbcd --- /dev/null +++ b/dom/security/test/csp/browser_test_web_manifest_mixed_content.js @@ -0,0 +1,53 @@ +/* + * Description of the test: + * Check that mixed content blocker works prevents fetches of + * mixed content manifests. + */ +/*globals Cu, ok*/ +"use strict"; +const { + ManifestObtainer +} = Cu.import("resource://gre/modules/ManifestObtainer.jsm", {}); +const path = "/tests/dom/security/test/csp/"; +const mixedContent = `${path}file_web_manifest_mixed_content.html`; +const server = `${path}file_testserver.sjs`; +const secureURL = new URL(`https://example.com${server}`); +const tests = [ + // Trying to load mixed content in file_web_manifest_mixed_content.html + // needs to result in an error. + { + expected: "Mixed Content Blocker prevents fetching manifest.", + get tabURL() { + const url = new URL(secureURL); + url.searchParams.append("file", mixedContent); + return url.href; + }, + run(error) { + // Check reason for error. + const check = /NetworkError when attempting to fetch resource/.test(error.message); + ok(check, this.expected); + } + } +]; + +//jscs:disable +add_task(function* () { + //jscs:enable + const testPromises = tests.map((test) => { + const tabOptions = { + gBrowser, + url: test.tabURL, + skipAnimation: true, + }; + return BrowserTestUtils.withNewTab(tabOptions, (browser) => testObtainingManifest(browser, test)); + }); + yield Promise.all(testPromises); +}); + +function* testObtainingManifest(aBrowser, aTest) { + try { + yield ManifestObtainer.browserObtainManifest(aBrowser); + } catch (e) { + aTest.run(e); + } +} diff --git a/dom/security/test/csp/file_CSP.css b/dom/security/test/csp/file_CSP.css new file mode 100644 index 000000000..6835c4d4a --- /dev/null +++ b/dom/security/test/csp/file_CSP.css @@ -0,0 +1,20 @@ +/* + * Moved this CSS from an inline stylesheet to an external file when we added + * inline-style blocking in bug 763879. + * This test may hang if the load for this .css file is blocked due to a + * malfunction of CSP, but should pass if the style_good test passes. + */ + +/* CSS font embedding tests */ +@font-face { + font-family: "arbitrary_good"; + src: url('file_CSP.sjs?testid=font_good&type=application/octet-stream'); +} +@font-face { + font-family: "arbitrary_bad"; + src: url('http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=font_bad&type=application/octet-stream'); +} + +.div_arbitrary_good { font-family: "arbitrary_good"; } +.div_arbitrary_bad { font-family: "arbitrary_bad"; } + diff --git a/dom/security/test/csp/file_CSP.sjs b/dom/security/test/csp/file_CSP.sjs new file mode 100644 index 000000000..85c2df3ba --- /dev/null +++ b/dom/security/test/csp/file_CSP.sjs @@ -0,0 +1,26 @@ +// SJS file for CSP mochitests + +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + var isPreflight = request.method == "OPTIONS"; + + + //avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if ("type" in query) { + response.setHeader("Content-Type", unescape(query['type']), false); + } else { + response.setHeader("Content-Type", "text/html", false); + } + + if ("content" in query) { + response.write(unescape(query['content'])); + } +} diff --git a/dom/security/test/csp/file_allow_https_schemes.html b/dom/security/test/csp/file_allow_https_schemes.html new file mode 100644 index 000000000..787e683e8 --- /dev/null +++ b/dom/security/test/csp/file_allow_https_schemes.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 826805 - CSP: Allow http and https for scheme-less sources</title> + </head> + <body> + <div id="testdiv">blocked</div> + <!-- + We resue file_path_matching.js which just updates the contents of 'testdiv' to contain allowed. + Note, that we are loading the file_path_matchting.js using a scheme of 'https'. + --> + <script src="https://example.com/tests/dom/security/test/csp/file_path_matching.js#foo"></script> +</body> +</html> diff --git a/dom/security/test/csp/file_base_uri_server.sjs b/dom/security/test/csp/file_base_uri_server.sjs new file mode 100644 index 000000000..dfba2a061 --- /dev/null +++ b/dom/security/test/csp/file_base_uri_server.sjs @@ -0,0 +1,61 @@ +// Custom *.sjs file specifically for the needs of +// https://bugzilla.mozilla.org/show_bug.cgi?id=1263286 + +"use strict"; +Components.utils.importGlobalProperties(["URLSearchParams"]); + +const PRE_BASE = ` + <!DOCTYPE HTML> + <html> + <head> + <title>Bug 1045897 - Test CSP base-uri directive</title>`; + +const REGULAR_POST_BASE =` + </head> + <body onload='window.parent.postMessage({result: document.baseURI}, "*");'> + <!-- just making use of the 'base' tag for this test --> + </body> + </html>`; + +const SCRIPT_POST_BASE = ` + </head> + <body> + <script> + document.getElementById("base1").removeAttribute("href"); + window.parent.postMessage({result: document.baseURI}, "*"); + </script> + </body> + </html>`; + +function handleRequest(request, response) { + const query = new URLSearchParams(request.queryString); + + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // Deliver the CSP policy encoded in the URL + response.setHeader("Content-Security-Policy", query.get("csp"), false); + + // Send HTML to test allowed/blocked behaviors + response.setHeader("Content-Type", "text/html", false); + response.write(PRE_BASE); + var base1 = + "<base id=\"base1\" href=\"" + query.get("base1") + "\">"; + var base2 = + "<base id=\"base2\" href=\"" + query.get("base2") + "\">"; + response.write(base1 + base2); + + if (query.get("action") === "enforce-csp") { + response.write(REGULAR_POST_BASE); + return; + } + + if (query.get("action") === "remove-base1") { + response.write(SCRIPT_POST_BASE); + return; + } + + // we should never get here, but just in case + // return something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/csp/file_blob_data_schemes.html b/dom/security/test/csp/file_blob_data_schemes.html new file mode 100644 index 000000000..0a4a49160 --- /dev/null +++ b/dom/security/test/csp/file_blob_data_schemes.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1086999 - Wildcard should not match blob:, data:</title> +</head> +<body> +<script type="text/javascript"> + +var base64data = +"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + +"P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; + + +// construct an image element using *data:* +var data_src = "data:image/png;base64," + base64data; +var data_img = document.createElement('img'); +data_img.onload = function() { + window.parent.postMessage({scheme: "data", result: "allowed"}, "*"); +} +data_img.onerror = function() { + window.parent.postMessage({scheme: "data", result: "blocked"}, "*"); +} +data_img.src = data_src; +document.body.appendChild(data_img); + + +// construct an image element using *blob:* +var byteCharacters = atob(base64data); +var byteNumbers = new Array(byteCharacters.length); +for (var i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); +} +var byteArray = new Uint8Array(byteNumbers); +var blob = new Blob([byteArray], {type: "image/png"}); +var imageUrl = URL.createObjectURL( blob ); + +var blob_img = document.createElement('img'); +blob_img.onload = function() { + window.parent.postMessage({scheme: "blob", result: "allowed"}, "*"); +} +blob_img.onerror = function() { + window.parent.postMessage({scheme: "blob", result: "blocked"}, "*"); +} +blob_img.src = imageUrl; +document.body.appendChild(blob_img); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/file_block_all_mcb.sjs b/dom/security/test/csp/file_block_all_mcb.sjs new file mode 100644 index 000000000..731553dd7 --- /dev/null +++ b/dom/security/test/csp/file_block_all_mcb.sjs @@ -0,0 +1,76 @@ +// custom *.sjs for Bug 1122236 +// CSP: 'block-all-mixed-content' + +const HEAD = + "<!DOCTYPE HTML>" + + "<html><head><meta charset=\"utf-8\">" + + "<title>Bug 1122236 - CSP: Implement block-all-mixed-content</title>" + + "</head>"; + +const CSP_ALLOW = + "<meta http-equiv=\"Content-Security-Policy\" content=\"img-src *\">"; + +const CSP_BLOCK = + "<meta http-equiv=\"Content-Security-Policy\" content=\"block-all-mixed-content\">"; + +const BODY = + "<body>" + + "<img id=\"testimage\" src=\"http://mochi.test:8888/tests/image/test/mochitest/blue.png\"></img>" + + "<script type=\"application/javascript\">" + + " var myImg = document.getElementById(\"testimage\");" + + " myImg.onload = function(e) {" + + " window.parent.postMessage({result: \"img-loaded\"}, \"*\");" + + " };" + + " myImg.onerror = function(e) {" + + " window.parent.postMessage({result: \"img-blocked\"}, \"*\");" + + " };" + + "</script>" + + "</body>" + + "</html>"; + +// We have to use this special code fragment, in particular '?nocache' to trigger an +// actual network load rather than loading the image from the cache. +const BODY_CSPRO = + "<body>" + + "<img id=\"testimage\" src=\"http://mochi.test:8888/tests/image/test/mochitest/blue.png?nocache\"></img>" + + "<script type=\"application/javascript\">" + + " var myImg = document.getElementById(\"testimage\");" + + " myImg.onload = function(e) {" + + " window.parent.postMessage({result: \"img-loaded\"}, \"*\");" + + " };" + + " myImg.onerror = function(e) {" + + " window.parent.postMessage({result: \"img-blocked\"}, \"*\");" + + " };" + + "</script>" + + "</body>" + + "</html>"; + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + var queryString = request.queryString; + + if (queryString === "csp-block") { + response.write(HEAD + CSP_BLOCK + BODY); + return; + } + if (queryString === "csp-allow") { + response.write(HEAD + CSP_ALLOW + BODY); + return; + } + if (queryString === "no-csp") { + response.write(HEAD + BODY); + return; + } + if (queryString === "cspro-block") { + // CSP RO is not supported in meta tag, let's use the header + response.setHeader("Content-Security-Policy-Report-Only", "block-all-mixed-content", false); + response.write(HEAD + BODY_CSPRO); + return; + } + // we should never get here but just in case return something unexpected + response.write("do'h"); + +} diff --git a/dom/security/test/csp/file_block_all_mixed_content_frame_navigation1.html b/dom/security/test/csp/file_block_all_mixed_content_frame_navigation1.html new file mode 100644 index 000000000..fdc1ae87a --- /dev/null +++ b/dom/security/test/csp/file_block_all_mixed_content_frame_navigation1.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content="block-all-mixed-content"> + <title>Bug 1122236 - CSP: Implement block-all-mixed-content</title> +</head> +<body> +<b>user clicks and navigates from https://b.com to http://c.com</b> + +<a id="navlink" href="http://example.com/tests/dom/security/test/csp/file_block_all_mixed_content_frame_navigation2.html">foo</a> + +<script class="testbody" type="text/javascript"> + // click the link to start the frame navigation + document.getElementById("navlink").click(); +</script> + +</body> +</html> diff --git a/dom/security/test/csp/file_block_all_mixed_content_frame_navigation2.html b/dom/security/test/csp/file_block_all_mixed_content_frame_navigation2.html new file mode 100644 index 000000000..4c4084e9e --- /dev/null +++ b/dom/security/test/csp/file_block_all_mixed_content_frame_navigation2.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1122236 - CSP: Implement block-all-mixed-content</title> +</head> +<body> +<b>http://c.com loaded, let's tell the parent</b> + +<script class="testbody" type="text/javascript"> + window.parent.postMessage({result: "frame-navigated"}, "*"); +</script> + +</body> +</html> diff --git a/dom/security/test/csp/file_bug1229639.html b/dom/security/test/csp/file_bug1229639.html new file mode 100644 index 000000000..1e6152ead --- /dev/null +++ b/dom/security/test/csp/file_bug1229639.html @@ -0,0 +1,7 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + <!-- this should be allowed --> + <script src="http://mochi.test:8888/tests/dom/security/test/csp/%24.js"> </script> + </body> +</html> diff --git a/dom/security/test/csp/file_bug1229639.html^headers^ b/dom/security/test/csp/file_bug1229639.html^headers^ new file mode 100644 index 000000000..0177de7a3 --- /dev/null +++ b/dom/security/test/csp/file_bug1229639.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: "default-src 'self'; script-src http://mochi.test:8888/tests/dom/security/test/csp/%24.js
\ No newline at end of file diff --git a/dom/security/test/csp/file_bug1312272.html b/dom/security/test/csp/file_bug1312272.html new file mode 100644 index 000000000..18e0e5589 --- /dev/null +++ b/dom/security/test/csp/file_bug1312272.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <meta charset="utf-8"> + <title>marquee inline script tests for Bug 1312272</title> +</head> +<body> +<marquee id="m" onstart="parent.postMessage('csp-violation-marquee-onstart', '*')">bug 1312272</marquee> +<script src="file_bug1312272.js"></script> +</body> +</html> diff --git a/dom/security/test/csp/file_bug1312272.html^headers^ b/dom/security/test/csp/file_bug1312272.html^headers^ new file mode 100644 index 000000000..25a9483ea --- /dev/null +++ b/dom/security/test/csp/file_bug1312272.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src *; script-src * 'unsafe-eval' diff --git a/dom/security/test/csp/file_bug1312272.js b/dom/security/test/csp/file_bug1312272.js new file mode 100644 index 000000000..01c03e43f --- /dev/null +++ b/dom/security/test/csp/file_bug1312272.js @@ -0,0 +1,8 @@ +var m = document.getElementById("m"); +m.addEventListener("click", function() { + // this will trigger after onstart, obviously. + parent.postMessage('finish', '*'); +}); +console.log("finish-handler setup"); +m.click(); +console.log("clicked"); diff --git a/dom/security/test/csp/file_bug663567.xsl b/dom/security/test/csp/file_bug663567.xsl new file mode 100644 index 000000000..b12b0d3b1 --- /dev/null +++ b/dom/security/test/csp/file_bug663567.xsl @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="ISO-8859-1"?>
+<!-- Edited by XMLSpy® -->
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:template match="/">
+ <html>
+ <body>
+ <h2 id="xsltheader">this xml file should be formatted using an xsl file(lower iframe should contain xml dump)!</h2>
+ <table border="1">
+ <tr bgcolor="#990099">
+ <th>Title</th>
+ <th>Artist</th>
+ <th>Price</th>
+ </tr>
+ <xsl:for-each select="catalog/cd">
+ <tr>
+ <td><xsl:value-of select="title"/></td>
+ <td><xsl:value-of select="artist"/></td>
+ <td><xsl:value-of select="price"/></td>
+ </tr>
+ </xsl:for-each>
+ </table>
+ </body>
+ </html>
+</xsl:template>
+</xsl:stylesheet>
+
diff --git a/dom/security/test/csp/file_bug663567_allows.xml b/dom/security/test/csp/file_bug663567_allows.xml new file mode 100644 index 000000000..93d345103 --- /dev/null +++ b/dom/security/test/csp/file_bug663567_allows.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="ISO-8859-1"?>
+<?xml-stylesheet type="text/xsl" href="file_bug663567.xsl"?>
+<catalog>
+ <cd>
+ <title>Empire Burlesque</title>
+ <artist>Bob Dylan</artist>
+ <country>USA</country>
+ <company>Columbia</company>
+ <price>10.90</price>
+ <year>1985</year>
+ </cd>
+ <cd>
+ <title>Hide your heart</title>
+ <artist>Bonnie Tyler</artist>
+ <country>UK</country>
+ <company>CBS Records</company>
+ <price>9.90</price>
+ <year>1988</year>
+ </cd>
+ <cd>
+ <title>Greatest Hits</title>
+ <artist>Dolly Parton</artist>
+ <country>USA</country>
+ <company>RCA</company>
+ <price>9.90</price>
+ <year>1982</year>
+ </cd>
+</catalog>
diff --git a/dom/security/test/csp/file_bug663567_allows.xml^headers^ b/dom/security/test/csp/file_bug663567_allows.xml^headers^ new file mode 100644 index 000000000..4c6fa3c26 --- /dev/null +++ b/dom/security/test/csp/file_bug663567_allows.xml^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' diff --git a/dom/security/test/csp/file_bug663567_blocks.xml b/dom/security/test/csp/file_bug663567_blocks.xml new file mode 100644 index 000000000..93d345103 --- /dev/null +++ b/dom/security/test/csp/file_bug663567_blocks.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="ISO-8859-1"?>
+<?xml-stylesheet type="text/xsl" href="file_bug663567.xsl"?>
+<catalog>
+ <cd>
+ <title>Empire Burlesque</title>
+ <artist>Bob Dylan</artist>
+ <country>USA</country>
+ <company>Columbia</company>
+ <price>10.90</price>
+ <year>1985</year>
+ </cd>
+ <cd>
+ <title>Hide your heart</title>
+ <artist>Bonnie Tyler</artist>
+ <country>UK</country>
+ <company>CBS Records</company>
+ <price>9.90</price>
+ <year>1988</year>
+ </cd>
+ <cd>
+ <title>Greatest Hits</title>
+ <artist>Dolly Parton</artist>
+ <country>USA</country>
+ <company>RCA</company>
+ <price>9.90</price>
+ <year>1982</year>
+ </cd>
+</catalog>
diff --git a/dom/security/test/csp/file_bug663567_blocks.xml^headers^ b/dom/security/test/csp/file_bug663567_blocks.xml^headers^ new file mode 100644 index 000000000..baf7f3c6a --- /dev/null +++ b/dom/security/test/csp/file_bug663567_blocks.xml^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src *.example.com diff --git a/dom/security/test/csp/file_bug802872.html b/dom/security/test/csp/file_bug802872.html new file mode 100644 index 000000000..dc7129b0c --- /dev/null +++ b/dom/security/test/csp/file_bug802872.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 802872</title>
+ <!-- Including SimpleTest.js so we can use AddLoadEvent !-->
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <script src='file_bug802872.js'></script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_bug802872.html^headers^ b/dom/security/test/csp/file_bug802872.html^headers^ new file mode 100644 index 000000000..4c6fa3c26 --- /dev/null +++ b/dom/security/test/csp/file_bug802872.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' diff --git a/dom/security/test/csp/file_bug802872.js b/dom/security/test/csp/file_bug802872.js new file mode 100644 index 000000000..5df8086cc --- /dev/null +++ b/dom/security/test/csp/file_bug802872.js @@ -0,0 +1,43 @@ +/* + * The policy for this test is: + * Content-Security-Policy: default-src 'self' + */ + +function createAllowedEvent() { + /* + * Creates a new EventSource using 'http://mochi.test:8888'. Since all mochitests run on + * 'http://mochi.test', a default-src of 'self' allows this request. + */ + var src_event = new EventSource("http://mochi.test:8888/tests/dom/security/test/csp/file_bug802872.sjs"); + + src_event.onmessage = function(e) { + src_event.close(); + parent.dispatchEvent(new Event('allowedEventSrcCallbackOK')); + } + + src_event.onerror = function(e) { + src_event.close(); + parent.dispatchEvent(new Event('allowedEventSrcCallbackFailed')); + } +} + +function createBlockedEvent() { + /* + * creates a new EventSource using 'http://example.com'. This domain is not whitelisted by the + * CSP of this page, therefore the CSP blocks this request. + */ + var src_event = new EventSource("http://example.com/tests/dom/security/test/csp/file_bug802872.sjs"); + + src_event.onmessage = function(e) { + src_event.close(); + parent.dispatchEvent(new Event('blockedEventSrcCallbackOK')); + } + + src_event.onerror = function(e) { + src_event.close(); + parent.dispatchEvent(new Event('blockedEventSrcCallbackFailed')); + } +} + +addLoadEvent(createAllowedEvent); +addLoadEvent(createBlockedEvent); diff --git a/dom/security/test/csp/file_bug802872.sjs b/dom/security/test/csp/file_bug802872.sjs new file mode 100644 index 000000000..b3e3f7024 --- /dev/null +++ b/dom/security/test/csp/file_bug802872.sjs @@ -0,0 +1,7 @@ +function handleRequest(request, response) +{ + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/event-stream", false); + response.write("data: eventsource response from server!"); + response.write("\n\n"); +} diff --git a/dom/security/test/csp/file_bug836922_npolicies.html b/dom/security/test/csp/file_bug836922_npolicies.html new file mode 100644 index 000000000..6a728813a --- /dev/null +++ b/dom/security/test/csp/file_bug836922_npolicies.html @@ -0,0 +1,12 @@ +<html> + <head> + <link rel='stylesheet' type='text/css' + href='/tests/dom/security/test/csp/file_CSP.sjs?testid=css_self&type=text/css' /> + + </head> + <body> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img_self&type=img/png"> </img> + <script src='/tests/dom/security/test/csp/file_CSP.sjs?testid=script_self&type=text/javascript'></script> + + </body> +</html> diff --git a/dom/security/test/csp/file_bug836922_npolicies.html^headers^ b/dom/security/test/csp/file_bug836922_npolicies.html^headers^ new file mode 100644 index 000000000..ec6ba8c4a --- /dev/null +++ b/dom/security/test/csp/file_bug836922_npolicies.html^headers^ @@ -0,0 +1,2 @@ +content-security-policy: default-src 'self'; img-src 'none'; report-uri http://mochi.test:8888/tests/dom/security/test/csp/file_bug836922_npolicies_violation.sjs +content-security-policy-report-only: default-src *; img-src 'self'; script-src 'none'; report-uri http://mochi.test:8888/tests/dom/security/test/csp/file_bug836922_npolicies_ro_violation.sjs diff --git a/dom/security/test/csp/file_bug836922_npolicies_ro_violation.sjs b/dom/security/test/csp/file_bug836922_npolicies_ro_violation.sjs new file mode 100644 index 000000000..3e7603421 --- /dev/null +++ b/dom/security/test/csp/file_bug836922_npolicies_ro_violation.sjs @@ -0,0 +1,53 @@ +// SJS file that receives violation reports and then responds with nothing. + +const CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +const STATE_KEY = "bug836922_ro_violations"; + +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + if ('results' in query) { + // if asked for the received data, send it. + response.setHeader("Content-Type", "text/javascript", false); + if (getState(STATE_KEY)) { + response.write(getState(STATE_KEY)); + } else { + // no state has been recorded. + response.write(JSON.stringify({})); + } + } else if ('reset' in query) { + //clear state + setState(STATE_KEY, JSON.stringify(null)); + } else { + // ... otherwise, just respond "ok". + response.write("null"); + + var bodystream = new BinaryInputStream(request.bodyInputStream); + var avail; + var bytes = []; + while ((avail = bodystream.available()) > 0) + Array.prototype.push.apply(bytes, bodystream.readByteArray(avail)); + + var data = String.fromCharCode.apply(null, bytes); + + // figure out which test was violating a policy + var testpat = new RegExp("testid=([a-z0-9_]+)"); + var testid = testpat.exec(data)[1]; + + // store the violation in the persistent state + var s = JSON.parse(getState(STATE_KEY) || "{}"); + s[testid] ? s[testid]++ : s[testid] = 1; + setState(STATE_KEY, JSON.stringify(s)); + } +} + + diff --git a/dom/security/test/csp/file_bug836922_npolicies_violation.sjs b/dom/security/test/csp/file_bug836922_npolicies_violation.sjs new file mode 100644 index 000000000..15e4958af --- /dev/null +++ b/dom/security/test/csp/file_bug836922_npolicies_violation.sjs @@ -0,0 +1,59 @@ +// SJS file that receives violation reports and then responds with nothing. + +const CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +const STATE = "bug836922_violations"; + +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + + if ('results' in query) { + // if asked for the received data, send it. + response.setHeader("Content-Type", "text/javascript", false); + if (getState(STATE)) { + response.write(getState(STATE)); + } else { + // no state has been recorded. + response.write(JSON.stringify({})); + } + } else if ('reset' in query) { + //clear state + setState(STATE, JSON.stringify(null)); + } else { + // ... otherwise, just respond "ok". + response.write("null"); + + var bodystream = new BinaryInputStream(request.bodyInputStream); + var avail; + var bytes = []; + while ((avail = bodystream.available()) > 0) + Array.prototype.push.apply(bytes, bodystream.readByteArray(avail)); + + var data = String.fromCharCode.apply(null, bytes); + + // figure out which test was violating a policy + var testpat = new RegExp("testid=([a-z0-9_]+)"); + var testid = testpat.exec(data)[1]; + + // store the violation in the persistent state + var s = getState(STATE); + if (!s) s = "{}"; + s = JSON.parse(s); + if (!s) s = {}; + + if (!s[testid]) s[testid] = 0; + s[testid]++; + setState(STATE, JSON.stringify(s)); + } +} + + diff --git a/dom/security/test/csp/file_bug885433_allows.html b/dom/security/test/csp/file_bug885433_allows.html new file mode 100644 index 000000000..5d7aacbda --- /dev/null +++ b/dom/security/test/csp/file_bug885433_allows.html @@ -0,0 +1,38 @@ +<!doctype html> +<!-- +The Content-Security-Policy header for this file is: + + Content-Security-Policy: img-src 'self'; + +It does not include any of the default-src, script-src, or style-src +directives. It should allow the use of unsafe-inline and unsafe-eval on +scripts, and unsafe-inline on styles, because no directives related to scripts +or styles are specified. +--> +<html> +<body> + <ol> + <li id="unsafe-inline-script-allowed">Inline script allowed (this text should be green)</li> + <li id="unsafe-eval-script-allowed">Eval script allowed (this text should be green)</li> + <li id="unsafe-inline-style-allowed">Inline style allowed (this text should be green)</li> + </ol> + + <script> + // Use inline script to set a style attribute + document.getElementById("unsafe-inline-script-allowed").style.color = "green"; + + // Use eval to set a style attribute + // try/catch is used because CSP causes eval to throw an exception when it + // is blocked, which would derail the rest of the tests in this file. + try { + eval('document.getElementById("unsafe-eval-script-allowed").style.color = "green";'); + } catch (e) {} + </script> + + <style> + li#unsafe-inline-style-allowed { + color: green; + } + </style> +</body> +</html> diff --git a/dom/security/test/csp/file_bug885433_allows.html^headers^ b/dom/security/test/csp/file_bug885433_allows.html^headers^ new file mode 100644 index 000000000..767b9ca92 --- /dev/null +++ b/dom/security/test/csp/file_bug885433_allows.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: img-src 'self'; diff --git a/dom/security/test/csp/file_bug885433_blocks.html b/dom/security/test/csp/file_bug885433_blocks.html new file mode 100644 index 000000000..2279b33e4 --- /dev/null +++ b/dom/security/test/csp/file_bug885433_blocks.html @@ -0,0 +1,37 @@ +<!doctype html> +<!-- +The Content-Security-Policy header for this file is: + + Content-Security-Policy: default-src 'self'; + +The Content-Security-Policy header for this file includes the default-src +directive, which triggers the default behavior of blocking unsafe-inline and +unsafe-eval on scripts, and unsafe-inline on styles. +--> +<html> +<body> + <ol> + <li id="unsafe-inline-script-blocked">Inline script blocked (this text should be black)</li> + <li id="unsafe-eval-script-blocked">Eval script blocked (this text should be black)</li> + <li id="unsafe-inline-style-blocked">Inline style blocked (this text should be black)</li> + </ol> + + <script> + // Use inline script to set a style attribute + document.getElementById("unsafe-inline-script-blocked").style.color = "green"; + + // Use eval to set a style attribute + // try/catch is used because CSP causes eval to throw an exception when it + // is blocked, which would derail the rest of the tests in this file. + try { + eval('document.getElementById("unsafe-eval-script-blocked").style.color = "green";'); + } catch (e) {} + </script> + + <style> + li#unsafe-inline-style-blocked { + color: green; + } + </style> +</body> +</html> diff --git a/dom/security/test/csp/file_bug885433_blocks.html^headers^ b/dom/security/test/csp/file_bug885433_blocks.html^headers^ new file mode 100644 index 000000000..f82598b67 --- /dev/null +++ b/dom/security/test/csp/file_bug885433_blocks.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self'; diff --git a/dom/security/test/csp/file_bug886164.html b/dom/security/test/csp/file_bug886164.html new file mode 100644 index 000000000..ec8c9e7e9 --- /dev/null +++ b/dom/security/test/csp/file_bug886164.html @@ -0,0 +1,15 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + <!-- sandbox="allow-same-origin" --> + <!-- Content-Security-Policy: default-src 'self' --> + + <!-- these should be stopped by CSP --> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img_bad&type=img/png"> </img> + + <!-- these should load ok --> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img_good&type=img/png" /> + <script src='/tests/dom/security/test/csp/file_CSP.sjs?testid=scripta_bad&type=text/javascript'></script> + + </body> +</html> diff --git a/dom/security/test/csp/file_bug886164.html^headers^ b/dom/security/test/csp/file_bug886164.html^headers^ new file mode 100644 index 000000000..4c6fa3c26 --- /dev/null +++ b/dom/security/test/csp/file_bug886164.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' diff --git a/dom/security/test/csp/file_bug886164_2.html b/dom/security/test/csp/file_bug886164_2.html new file mode 100644 index 000000000..83d36c55a --- /dev/null +++ b/dom/security/test/csp/file_bug886164_2.html @@ -0,0 +1,14 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + <!-- sandbox --> + <!-- Content-Security-Policy: default-src 'self' --> + + <!-- these should be stopped by CSP --> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img2_bad&type=img/png"> </img> + + <!-- these should load ok --> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img2a_good&type=img/png" /> + + </body> +</html> diff --git a/dom/security/test/csp/file_bug886164_2.html^headers^ b/dom/security/test/csp/file_bug886164_2.html^headers^ new file mode 100644 index 000000000..4c6fa3c26 --- /dev/null +++ b/dom/security/test/csp/file_bug886164_2.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' diff --git a/dom/security/test/csp/file_bug886164_3.html b/dom/security/test/csp/file_bug886164_3.html new file mode 100644 index 000000000..8b4313000 --- /dev/null +++ b/dom/security/test/csp/file_bug886164_3.html @@ -0,0 +1,12 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + <!-- sandbox --> + <!-- Content-Security-Policy: default-src 'none' --> + + <!-- these should be stopped by CSP --> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img3_bad&type=img/png"> </img> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img3a_bad&type=img/png" /> + + </body> +</html> diff --git a/dom/security/test/csp/file_bug886164_3.html^headers^ b/dom/security/test/csp/file_bug886164_3.html^headers^ new file mode 100644 index 000000000..6581fd425 --- /dev/null +++ b/dom/security/test/csp/file_bug886164_3.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'none' diff --git a/dom/security/test/csp/file_bug886164_4.html b/dom/security/test/csp/file_bug886164_4.html new file mode 100644 index 000000000..41137ea01 --- /dev/null +++ b/dom/security/test/csp/file_bug886164_4.html @@ -0,0 +1,12 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + <!-- sandbox --> + <!-- Content-Security-Policy: default-src 'none' --> + + <!-- these should be stopped by CSP --> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img4_bad&type=img/png"> </img> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img4a_bad&type=img/png" /> + + </body> +</html> diff --git a/dom/security/test/csp/file_bug886164_4.html^headers^ b/dom/security/test/csp/file_bug886164_4.html^headers^ new file mode 100644 index 000000000..6581fd425 --- /dev/null +++ b/dom/security/test/csp/file_bug886164_4.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'none' diff --git a/dom/security/test/csp/file_bug886164_5.html b/dom/security/test/csp/file_bug886164_5.html new file mode 100644 index 000000000..ae65171a5 --- /dev/null +++ b/dom/security/test/csp/file_bug886164_5.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> <meta charset="utf-8"> </head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.postMessage({ok: result, desc: desc}, "*"); + } + + function doStuff() { + ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts"); + } +</script> +<script src='file_iframe_sandbox_pass.js'></script> +<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'> + I am sandboxed but with only inline "allow-scripts" + + <!-- sandbox="allow-scripts" --> + <!-- Content-Security-Policy: default-src 'none' 'unsafe-inline'--> + + <!-- these should be stopped by CSP --> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img5_bad&type=img/png" /> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img5a_bad&type=img/png"> </img> + <script src='/tests/dom/security/test/csp/file_CSP.sjs?testid=script5_bad&type=text/javascript'></script> + <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script5a_bad&type=text/javascript'></script> +</body> +</html> diff --git a/dom/security/test/csp/file_bug886164_5.html^headers^ b/dom/security/test/csp/file_bug886164_5.html^headers^ new file mode 100644 index 000000000..3abc19055 --- /dev/null +++ b/dom/security/test/csp/file_bug886164_5.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'none' 'unsafe-inline'; diff --git a/dom/security/test/csp/file_bug886164_6.html b/dom/security/test/csp/file_bug886164_6.html new file mode 100644 index 000000000..f985ec8ce --- /dev/null +++ b/dom/security/test/csp/file_bug886164_6.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.postMessage({ok: result, desc: desc}, "*"); + } + + function doStuff() { + ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts"); + + document.getElementById('a_form').submit(); + + // trigger the javascript: url test + sendMouseEvent({type:'click'}, 'a_link'); + } +</script> +<script src='file_iframe_sandbox_pass.js'></script> +<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'> + I am sandboxed but with "allow-scripts" + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img6_bad&type=img/png"> </img> + <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script6_bad&type=text/javascript'></script> + + <form method="get" action="file_iframe_sandbox_form_fail.html" id="a_form"> + First name: <input type="text" name="firstname"> + Last name: <input type="text" name="lastname"> + <input type="submit" onclick="doSubmit()" id="a_button"> + </form> + + <a href = 'javascript:ok(true, "documents sandboxed with allow-scripts should be able to run script from javascript: URLs");' id='a_link'>click me</a> +</body> +</html> diff --git a/dom/security/test/csp/file_bug886164_6.html^headers^ b/dom/security/test/csp/file_bug886164_6.html^headers^ new file mode 100644 index 000000000..6f9fc3f25 --- /dev/null +++ b/dom/security/test/csp/file_bug886164_6.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' 'unsafe-inline'; diff --git a/dom/security/test/csp/file_bug888172.html b/dom/security/test/csp/file_bug888172.html new file mode 100644 index 000000000..27cf9b00a --- /dev/null +++ b/dom/security/test/csp/file_bug888172.html @@ -0,0 +1,28 @@ +<!doctype html> +<html> + <body> + <ol> + <li id="unsafe-inline-script">Inline script (green if allowed, black if blocked)</li> + <li id="unsafe-eval-script">Eval script (green if allowed, black if blocked)</li> + <li id="unsafe-inline-style">Inline style (green if allowed, black if blocked)</li> + </ol> + + <script> + // Use inline script to set a style attribute + document.getElementById("unsafe-inline-script").style.color = "green"; + + // Use eval to set a style attribute + // try/catch is used because CSP causes eval to throw an exception when it + // is blocked, which would derail the rest of the tests in this file. + try { + eval('document.getElementById("unsafe-eval-script").style.color = "green";'); + } catch (e) {} + </script> + + <style> + li#unsafe-inline-style { + color: green; + } + </style> + </body> +</html> diff --git a/dom/security/test/csp/file_bug888172.sjs b/dom/security/test/csp/file_bug888172.sjs new file mode 100644 index 000000000..03309610f --- /dev/null +++ b/dom/security/test/csp/file_bug888172.sjs @@ -0,0 +1,43 @@ +// SJS file for CSP mochitests + +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +function loadHTMLFromFile(path) { + // Load the HTML to return in the response from file. + // Since it's relative to the cwd of the test runner, we start there and + // append to get to the actual path of the file. + var testHTMLFile = + Components.classes["@mozilla.org/file/directory_service;1"]. + getService(Components.interfaces.nsIProperties). + get("CurWorkD", Components.interfaces.nsILocalFile); + var dirs = path.split("/"); + for (var i = 0; i < dirs.length; i++) { + testHTMLFile.append(dirs[i]); + } + var testHTMLFileStream = + Components.classes["@mozilla.org/network/file-input-stream;1"]. + createInstance(Components.interfaces.nsIFileInputStream); + testHTMLFileStream.init(testHTMLFile, -1, 0, 0); + var testHTML = NetUtil.readInputStreamToString(testHTMLFileStream, testHTMLFileStream.available()); + return testHTML; +} + +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // Deliver the CSP policy encoded in the URI + if (query['csp']) + response.setHeader("Content-Security-Policy", unescape(query['csp']), false); + + // Send HTML to test allowed/blocked behaviors + response.setHeader("Content-Type", "text/html", false); + response.write(loadHTMLFromFile("tests/dom/security/test/csp/file_bug888172.html")); +} diff --git a/dom/security/test/csp/file_bug909029_none.html b/dom/security/test/csp/file_bug909029_none.html new file mode 100644 index 000000000..0d4934a4a --- /dev/null +++ b/dom/security/test/csp/file_bug909029_none.html @@ -0,0 +1,20 @@ +<!doctype html> +<html> + <head> + <!-- file_CSP.sjs mocks a resource load --> + <link rel='stylesheet' type='text/css' + href='file_CSP.sjs?testid=noneExternalStylesBlocked&type=text/css' /> + </head> + <body> + <p id="inline-style">This should be green</p> + <p id="inline-script">This should be black</p> + <style> + p#inline-style { color:rgb(0, 128, 0); } + </style> + <script> + // Use inline script to set a style attribute + document.getElementById("inline-script").style.color = "rgb(0, 128, 0)"; + </script> + <img src="file_CSP.sjs?testid=noneExternalImgLoaded&type=img/png" /> + </body> +</html> diff --git a/dom/security/test/csp/file_bug909029_none.html^headers^ b/dom/security/test/csp/file_bug909029_none.html^headers^ new file mode 100644 index 000000000..ecb345875 --- /dev/null +++ b/dom/security/test/csp/file_bug909029_none.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src * ; style-src 'none' 'unsafe-inline'; diff --git a/dom/security/test/csp/file_bug909029_star.html b/dom/security/test/csp/file_bug909029_star.html new file mode 100644 index 000000000..bcb907a96 --- /dev/null +++ b/dom/security/test/csp/file_bug909029_star.html @@ -0,0 +1,19 @@ +<!doctype html> +<html> + <head> + <link rel='stylesheet' type='text/css' + href='file_CSP.sjs?testid=starExternalStylesLoaded&type=text/css' /> + </head> + <body> + <p id="inline-style">This should be green</p> + <p id="inline-script">This should be black</p> + <style> + p#inline-style { color:rgb(0, 128, 0); } + </style> + <script> + // Use inline script to set a style attribute + document.getElementById("inline-script").style.color = "rgb(0, 128, 0)"; + </script> + <img src="file_CSP.sjs?testid=starExternalImgLoaded&type=img/png" /> + </body> +</html> diff --git a/dom/security/test/csp/file_bug909029_star.html^headers^ b/dom/security/test/csp/file_bug909029_star.html^headers^ new file mode 100644 index 000000000..eccc1c011 --- /dev/null +++ b/dom/security/test/csp/file_bug909029_star.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src *; style-src * 'unsafe-inline'; diff --git a/dom/security/test/csp/file_bug910139.sjs b/dom/security/test/csp/file_bug910139.sjs new file mode 100644 index 000000000..172cc09c9 --- /dev/null +++ b/dom/security/test/csp/file_bug910139.sjs @@ -0,0 +1,52 @@ +// Server side js file for bug 910139, see file test_bug910139.html for details. + +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +function loadResponseFromFile(path) { + var testHTMLFile = + Components.classes["@mozilla.org/file/directory_service;1"]. + getService(Components.interfaces.nsIProperties). + get("CurWorkD", Components.interfaces.nsILocalFile); + var dirs = path.split("/"); + for (var i = 0; i < dirs.length; i++) { + testHTMLFile.append(dirs[i]); + } + var testHTMLFileStream = + Components.classes["@mozilla.org/network/file-input-stream;1"]. + createInstance(Components.interfaces.nsIFileInputStream); + testHTMLFileStream.init(testHTMLFile, -1, 0, 0); + var testHTML = NetUtil.readInputStreamToString(testHTMLFileStream, testHTMLFileStream.available()); + return testHTML; +} + +var policies = [ + "default-src 'self'; script-src 'self'", // CSP for checkAllowed + "default-src 'self'; script-src *.example.com" // CSP for checkBlocked +] + +function getPolicy() { + var index; + // setState only accepts strings as arguments + if (!getState("counter")) { + index = 0; + setState("counter", index.toString()); + } + else { + index = parseInt(getState("counter")); + ++index; + setState("counter", index.toString()); + } + return policies[index]; +} + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // set the required CSP + response.setHeader("Content-Security-Policy", getPolicy(), false); + + // return the requested XML file. + response.write(loadResponseFromFile("tests/dom/security/test/csp/file_bug910139.xml")); +} diff --git a/dom/security/test/csp/file_bug910139.xml b/dom/security/test/csp/file_bug910139.xml new file mode 100644 index 000000000..29feba941 --- /dev/null +++ b/dom/security/test/csp/file_bug910139.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<?xml-stylesheet type="text/xsl" href="file_bug910139.xsl"?> +<catalog> + <cd> + <title>Empire Burlesque</title> + <artist>Bob Dylan</artist> + <country>USA</country> + <company>Columbia</company> + <price>10.90</price> + <year>1985</year> + </cd> + <cd> + <title>Hide your heart</title> + <artist>Bonnie Tyler</artist> + <country>UK</country> + <company>CBS Records</company> + <price>9.90</price> + <year>1988</year> + </cd> + <cd> + <title>Greatest Hits</title> + <artist>Dolly Parton</artist> + <country>USA</country> + <company>RCA</company> + <price>9.90</price> + <year>1982</year> + </cd> +</catalog> diff --git a/dom/security/test/csp/file_bug910139.xsl b/dom/security/test/csp/file_bug910139.xsl new file mode 100644 index 000000000..b99abca09 --- /dev/null +++ b/dom/security/test/csp/file_bug910139.xsl @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<!-- Edited by XMLSpy® --> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:template match="/"> + <html> + <body> + <h2 id="xsltheader">this xml file should be formatted using an xsl file(lower iframe should contain xml dump)!</h2> + <table border="1"> + <tr bgcolor="#990099"> + <th>Title</th> + <th>Artist</th> + <th>Price</th> + </tr> + <xsl:for-each select="catalog/cd"> + <tr> + <td><xsl:value-of select="title"/></td> + <td><xsl:value-of select="artist"/></td> + <td><xsl:value-of select="price"/></td> + </tr> + </xsl:for-each> + </table> + </body> + </html> +</xsl:template> +</xsl:stylesheet> + diff --git a/dom/security/test/csp/file_bug941404.html b/dom/security/test/csp/file_bug941404.html new file mode 100644 index 000000000..3a2e636e0 --- /dev/null +++ b/dom/security/test/csp/file_bug941404.html @@ -0,0 +1,27 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + + <!-- this should be allowed (no CSP)--> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img_good&type=img/png"> </img> + + + <script type="text/javascript"> + var req = new XMLHttpRequest(); + req.onload = function() { + //this should be allowed (no CSP) + try { + var img = document.createElement("img"); + img.src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img2_good&type=img/png"; + document.body.appendChild(img); + } catch(e) { + console.log("yo: "+e); + } + }; + req.open("get", "file_bug941404_xhr.html", true); + req.responseType = "document"; + req.send(); + </script> + + </body> +</html> diff --git a/dom/security/test/csp/file_bug941404_xhr.html b/dom/security/test/csp/file_bug941404_xhr.html new file mode 100644 index 000000000..22e176f20 --- /dev/null +++ b/dom/security/test/csp/file_bug941404_xhr.html @@ -0,0 +1,5 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + </body> +</html> diff --git a/dom/security/test/csp/file_bug941404_xhr.html^headers^ b/dom/security/test/csp/file_bug941404_xhr.html^headers^ new file mode 100644 index 000000000..1e5f70cc3 --- /dev/null +++ b/dom/security/test/csp/file_bug941404_xhr.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'none' 'unsafe-inline' 'unsafe-eval' diff --git a/dom/security/test/csp/file_child-src_iframe.html b/dom/security/test/csp/file_child-src_iframe.html new file mode 100644 index 000000000..3534aa329 --- /dev/null +++ b/dom/security/test/csp/file_child-src_iframe.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1045891</title> + </head> + <body> + <iframe id="testframe"> </iframe> + <script type="text/javascript"> + page_id = window.location.hash.substring(1); + + function executeTest(ev) { + testframe = document.getElementById('testframe'); + testframe.contentWindow.postMessage({id:page_id, message:"execute"}, 'http://mochi.test:8888'); + } + + function reportError(ev) { + window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888'); + cleanup(); + } + + function recvMessage(ev) { + if (ev.data.id == page_id) { + window.parent.postMessage({id:ev.data.id, message:ev.data.message}, 'http://mochi.test:8888'); + cleanup(); + } + } + + function cleanup() { + testframe = document.getElementById('testframe'); + window.removeEventListener('message', recvMessage); + testframe.removeEventListener('load', executeTest); + testframe.removeEventListener('error', reportError); + } + + + window.addEventListener('message', recvMessage, false); + + try { + // Please note that file_testserver.sjs?foo does not return a response. + // For testing purposes this is not necessary because we only want to check + // whether CSP allows or blocks the load. + src = "file_testserver.sjs"; + src += "?file=" + escape("tests/dom/security/test/csp/file_child-src_inner_frame.html"); + src += "#" + escape(page_id); + testframe = document.getElementById('testframe'); + + testframe.addEventListener('load', executeTest, false); + testframe.addEventListener('error', reportError, false); + + testframe.src = src; + } + catch (e) { + if (e.message.match(/Failed to load script/)) { + window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888'); + } else { + window.parent.postMessage({id:page_id, message:"exception"}, 'http://mochi.test:8888'); + } + } + </script> + </body> +</html> diff --git a/dom/security/test/csp/file_child-src_inner_frame.html b/dom/security/test/csp/file_child-src_inner_frame.html new file mode 100644 index 000000000..e42102430 --- /dev/null +++ b/dom/security/test/csp/file_child-src_inner_frame.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1045891</title> + </head> + <body> + <iframe id="innermosttestframe"> </iframe> + <script type="text/javascript"> + page_id = window.location.hash.substring(1); + + function recvMessage(ev) { + if (ev.data.id == page_id) { + window.parent.postMessage({id:ev.data.id, message:'allowed'}, 'http://mochi.test:8888'); + window.removeEventListener('message', recvMessage); + } + } + + window.addEventListener('message', recvMessage, false); + </script> + </body> +</html> diff --git a/dom/security/test/csp/file_child-src_service_worker.html b/dom/security/test/csp/file_child-src_service_worker.html new file mode 100644 index 000000000..b291a4a4e --- /dev/null +++ b/dom/security/test/csp/file_child-src_service_worker.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1045891</title> + </head> + <body> + <script type="text/javascript"> + page_id = window.location.hash.substring(1); + try { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register( + 'file_child-src_service_worker.js', + { scope: './' + page_id + '/' } + ).then(function(reg) + { + // registration worked + reg.unregister().then(function() { + window.parent.postMessage({id:page_id, message:"allowed"}, 'http://mochi.test:8888'); + }); + }).catch(function(error) { + // registration failed + window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888'); + }); + }; + } catch(ex) { + window.parent.postMessage({id:page_id, message:"exception"}, 'http://mochi.test:8888'); + } + </script> + </body> +</html> diff --git a/dom/security/test/csp/file_child-src_service_worker.js b/dom/security/test/csp/file_child-src_service_worker.js new file mode 100644 index 000000000..53f768707 --- /dev/null +++ b/dom/security/test/csp/file_child-src_service_worker.js @@ -0,0 +1,3 @@ +this.addEventListener('install', function(event) { + close(); +}); diff --git a/dom/security/test/csp/file_child-src_shared_worker-redirect.html b/dom/security/test/csp/file_child-src_shared_worker-redirect.html new file mode 100644 index 000000000..313915302 --- /dev/null +++ b/dom/security/test/csp/file_child-src_shared_worker-redirect.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1045891</title> + </head> + <body> + <script type="text/javascript"> + page_id = window.location.hash.substring(1); + var redir = 'none'; + + page_id.split('_').forEach(function (val) { + var [name, value] = val.split('-'); + if (name == 'redir') { + redir = unescape(value); + } + }); + + try { + worker = new SharedWorker('file_redirect_worker.sjs?path=' + + escape("/tests/dom/security/test/csp/file_child-src_shared_worker.js") + + "&redir=" + redir + + "&page_id=" + page_id, + page_id); + worker.port.start(); + + worker.onerror = function(evt) { + evt.preventDefault(); + window.parent.postMessage({id:page_id, message:"blocked"}, + 'http://mochi.test:8888'); + } + + worker.port.onmessage = function(ev) { + window.parent.postMessage({id:page_id, message:"allowed"}, 'http://mochi.test:8888'); + }; + + worker.onerror = function() { + window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888'); + }; + + worker.port.postMessage('foo'); + } + catch (e) { + window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888'); + } + </script> + </body> +</html> diff --git a/dom/security/test/csp/file_child-src_shared_worker.html b/dom/security/test/csp/file_child-src_shared_worker.html new file mode 100644 index 000000000..0e9a56a29 --- /dev/null +++ b/dom/security/test/csp/file_child-src_shared_worker.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1045891</title> + </head> + <body> + <script type="text/javascript"> + page_id = window.location.hash.substring(1); + try { + worker = new SharedWorker( + 'file_testserver.sjs?file='+ + escape("tests/dom/security/test/csp/file_child-src_shared_worker.js"), + page_id); + worker.port.start(); + + worker.onerror = function(evt) { + evt.preventDefault(); + window.parent.postMessage({id:page_id, message:"blocked"}, + 'http://mochi.test:8888'); + } + + worker.port.onmessage = function(ev) { + window.parent.postMessage({id:page_id, message:"allowed"}, + 'http://mochi.test:8888'); + }; + worker.port.postMessage('foo'); + } + catch (e) { + window.parent.postMessage({id:page_id, message:"blocked"}, + 'http://mochi.test:8888'); + } + </script> + </body> +</html> diff --git a/dom/security/test/csp/file_child-src_shared_worker.js b/dom/security/test/csp/file_child-src_shared_worker.js new file mode 100644 index 000000000..0fca1394c --- /dev/null +++ b/dom/security/test/csp/file_child-src_shared_worker.js @@ -0,0 +1,8 @@ +onconnect = function(e) { + var port = e.ports[0]; + port.addEventListener('message', function(e) { + port.postMessage('success'); + }); + + port.start(); +} diff --git a/dom/security/test/csp/file_child-src_shared_worker_data.html b/dom/security/test/csp/file_child-src_shared_worker_data.html new file mode 100644 index 000000000..a4befe4ca --- /dev/null +++ b/dom/security/test/csp/file_child-src_shared_worker_data.html @@ -0,0 +1,37 @@ + +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1045891</title> + </head> + <body> + <script type="text/javascript"> + var page_id = window.location.hash.substring(1); + var shared_worker = "onconnect = function(e) { " + + "var port = e.ports[0];" + + "port.addEventListener('message'," + + "function(e) { port.postMessage('success'); });" + + "port.start(); }"; + + try { + var worker = new SharedWorker('data:application/javascript;charset=UTF-8,'+ + escape(shared_worker), page_id); + worker.port.start(); + + worker.onerror = function(evt) { + evt.preventDefault(); + window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888'); + } + + worker.port.onmessage = function(ev) { + window.parent.postMessage({id:page_id, message:"allowed"}, 'http://mochi.test:8888'); + }; + + worker.port.postMessage('foo'); + } + catch (e) { + window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888'); + } + </script> + </body> +</html> diff --git a/dom/security/test/csp/file_child-src_worker-redirect.html b/dom/security/test/csp/file_child-src_worker-redirect.html new file mode 100644 index 000000000..188f173b8 --- /dev/null +++ b/dom/security/test/csp/file_child-src_worker-redirect.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1045891</title> + </head> + <body> + <script type="text/javascript"> + var page_id = window.location.hash.substring(1); + var redir = 'none'; + + page_id.split('_').forEach(function (val) { + var [name, value] = val.split('-'); + if (name == 'redir') { + redir = unescape(value); + } + }); + + try { + worker = new Worker('file_redirect_worker.sjs?path=' + + escape("/tests/dom/security/test/csp/file_child-src_worker.js") + + "&redir=" + redir + + "&page_id=" + page_id + ); + + worker.onerror = function(error) { + var msg = error.message; + if (msg.match(/^NetworkError/) || msg.match(/Failed to load worker script/)) { + // this means CSP blocked it + msg = "blocked"; + } + window.parent.postMessage({id:page_id, message:msg}, 'http://mochi.test:8888'); + error.preventDefault(); + }; + + worker.onmessage = function(ev) { + window.parent.postMessage({id:page_id, message:"allowed"}, 'http://mochi.test:8888'); + + }; + worker.postMessage('foo'); + } + catch (e) { + if (e.message.match(/Failed to load script/)) { + window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888'); + } else { + window.parent.postMessage({id:page_id, message:"exception"}, 'http://mochi.test:8888'); + } + } + </script> + </body> +</html> diff --git a/dom/security/test/csp/file_child-src_worker.html b/dom/security/test/csp/file_child-src_worker.html new file mode 100644 index 000000000..9300d3f61 --- /dev/null +++ b/dom/security/test/csp/file_child-src_worker.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1045891</title> + </head> + <body> + <script type="text/javascript"> + page_id = window.location.hash.substring(1); + try { + worker = new Worker('file_testserver.sjs?file='+escape("tests/dom/security/test/csp/file_child-src_worker.js")); + + worker.onerror = function(e) { + window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888'); + e.preventDefault(); + } + + worker.onmessage = function(ev) { + window.parent.postMessage({id:page_id, message:"allowed"}, 'http://mochi.test:8888'); + } + + worker.postMessage('foo'); + } + catch (e) { + if (e.message.match(/Failed to load script/)) { + window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888'); + } else { + window.parent.postMessage({id:page_id, message:"exception"}, 'http://mochi.test:8888'); + } + } + </script> + </body> +</html> diff --git a/dom/security/test/csp/file_child-src_worker.js b/dom/security/test/csp/file_child-src_worker.js new file mode 100644 index 000000000..1d93cac6b --- /dev/null +++ b/dom/security/test/csp/file_child-src_worker.js @@ -0,0 +1,4 @@ +onmessage = function(e) { + postMessage('worker'); +}; + diff --git a/dom/security/test/csp/file_child-src_worker_data.html b/dom/security/test/csp/file_child-src_worker_data.html new file mode 100644 index 000000000..e9e22f01d --- /dev/null +++ b/dom/security/test/csp/file_child-src_worker_data.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1045891</title> + </head> + <body> + <script type="text/javascript"> + page_id = window.location.hash.substring(1); + try { + worker = new Worker('data:application/javascript;charset=UTF-8,'+escape('onmessage = function(e) { postMessage("worker"); };')); + + worker.onerror = function(e) { + window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888'); + e.preventDefault(); + } + + worker.onmessage = function(ev) { + window.parent.postMessage({id:page_id, message:"allowed"}, 'http://mochi.test:8888'); + } + + worker.postMessage('foo'); + } + catch (e) { + if (e.message.match(/Failed to load script/)) { + window.parent.postMessage({id:page_id, message:"blocked"}, 'http://mochi.test:8888'); + } else { + console.log(e); + window.parent.postMessage({id:page_id, message:"exception"}, 'http://mochi.test:8888'); + } + } + </script> + </body> +</html> diff --git a/dom/security/test/csp/file_child_worker.js b/dom/security/test/csp/file_child_worker.js new file mode 100644 index 000000000..256234377 --- /dev/null +++ b/dom/security/test/csp/file_child_worker.js @@ -0,0 +1,39 @@ +function doXHR(uri) { + try { + var xhr = new XMLHttpRequest(); + xhr.open("GET", uri); + xhr.send(); + } catch(ex) {} +} + +var sameBase = "http://mochi.test:8888/tests/dom/security/test/csp/file_CSP.sjs?testid="; +var crossBase = "http://example.com/tests/dom/security/test/csp/file_CSP.sjs?testid="; + +onmessage = (e) => { + for (base of [sameBase, crossBase]) { + var prefix; + var suffix; + if (e.data.inherited == "parent") { + //Worker inherits CSP from parent worker + prefix = base + "worker_child_inherited_parent_"; + suffix = base == sameBase ? "_good" : "_bad"; + } else if (e.data.inherited == "document") { + //Worker inherits CSP from owner document -> parent worker -> subworker + prefix = base + "worker_child_inherited_document_"; + suffix = base == sameBase ? "_good" : "_bad"; + } else { + // Worker delivers CSP from HTTP header + prefix = base + "worker_child_"; + suffix = base == sameBase ? "_same_bad" : "_cross_bad"; + } + + doXHR(prefix + "xhr" + suffix); + // Fetch is likely failed in subworker + // See Bug 1273070 - Failed to fetch in subworker + // Enable fetch test after the bug is fixed + // fetch(prefix + "xhr" + suffix); + try { + importScripts(prefix + "script" + suffix); + } catch(ex) {} + } +} diff --git a/dom/security/test/csp/file_child_worker.js^headers^ b/dom/security/test/csp/file_child_worker.js^headers^ new file mode 100644 index 000000000..6581fd425 --- /dev/null +++ b/dom/security/test/csp/file_child_worker.js^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'none' diff --git a/dom/security/test/csp/file_connect-src-fetch.html b/dom/security/test/csp/file_connect-src-fetch.html new file mode 100644 index 000000000..ff9b2f740 --- /dev/null +++ b/dom/security/test/csp/file_connect-src-fetch.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1139667 - Test mapping of fetch() to connect-src</title> + </head> + <body> + <script type="text/javascript"> + + // Please note that file_testserver.sjs?foo does not return a response. + // For testing purposes this is not necessary because we only want to check + // whether CSP allows or blocks the load. + fetch( "file_testserver.sjs?foo"); + + </script> +</body> +</html> diff --git a/dom/security/test/csp/file_connect-src.html b/dom/security/test/csp/file_connect-src.html new file mode 100644 index 000000000..17a940a0e --- /dev/null +++ b/dom/security/test/csp/file_connect-src.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1031530 - Test mapping of XMLHttpRequest to connect-src</title> + </head> + <body> + <script type="text/javascript"> + + try { + // Please note that file_testserver.sjs?foo does not return a response. + // For testing purposes this is not necessary because we only want to check + // whether CSP allows or blocks the load. + var xhr = new XMLHttpRequest(); + xhr.open("GET", "file_testserver.sjs?foo", false); + xhr.send(null); + } + catch (e) { } + + </script> +</body> +</html> diff --git a/dom/security/test/csp/file_data-uri_blocked.html b/dom/security/test/csp/file_data-uri_blocked.html new file mode 100644 index 000000000..293e857e3 --- /dev/null +++ b/dom/security/test/csp/file_data-uri_blocked.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1242019 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 587377</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <img width='1' height='1' title='' alt='' src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKTWlDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVN3WJP3Fj7f92UPVkLY8LGXbIEAIiOsCMgQWaIQkgBhhBASQMWFiApWFBURnEhVxILVCkidiOKgKLhnQYqIWotVXDjuH9yntX167+3t+9f7vOec5/zOec8PgBESJpHmomoAOVKFPDrYH49PSMTJvYACFUjgBCAQ5svCZwXFAADwA3l4fnSwP/wBr28AAgBw1S4kEsfh/4O6UCZXACCRAOAiEucLAZBSAMguVMgUAMgYALBTs2QKAJQAAGx5fEIiAKoNAOz0ST4FANipk9wXANiiHKkIAI0BAJkoRyQCQLsAYFWBUiwCwMIAoKxAIi4EwK4BgFm2MkcCgL0FAHaOWJAPQGAAgJlCLMwAIDgCAEMeE80DIEwDoDDSv+CpX3CFuEgBAMDLlc2XS9IzFLiV0Bp38vDg4iHiwmyxQmEXKRBmCeQinJebIxNI5wNMzgwAABr50cH+OD+Q5+bk4eZm52zv9MWi/mvwbyI+IfHf/ryMAgQAEE7P79pf5eXWA3DHAbB1v2upWwDaVgBo3/ldM9sJoFoK0Hr5i3k4/EAenqFQyDwdHAoLC+0lYqG9MOOLPv8z4W/gi372/EAe/tt68ABxmkCZrcCjg/1xYW52rlKO58sEQjFu9+cj/seFf/2OKdHiNLFcLBWK8ViJuFAiTcd5uVKRRCHJleIS6X8y8R+W/QmTdw0ArIZPwE62B7XLbMB+7gECiw5Y0nYAQH7zLYwaC5EAEGc0Mnn3AACTv/mPQCsBAM2XpOMAALzoGFyolBdMxggAAESggSqwQQcMwRSswA6cwR28wBcCYQZEQAwkwDwQQgbkgBwKoRiWQRlUwDrYBLWwAxqgEZrhELTBMTgN5+ASXIHrcBcGYBiewhi8hgkEQcgIE2EhOogRYo7YIs4IF5mOBCJhSDSSgKQg6YgUUSLFyHKkAqlCapFdSCPyLXIUOY1cQPqQ28ggMor8irxHMZSBslED1AJ1QLmoHxqKxqBz0XQ0D12AlqJr0Rq0Hj2AtqKn0UvodXQAfYqOY4DRMQ5mjNlhXIyHRWCJWBomxxZj5Vg1Vo81Yx1YN3YVG8CeYe8IJAKLgBPsCF6EEMJsgpCQR1hMWEOoJewjtBK6CFcJg4Qxwicik6hPtCV6EvnEeGI6sZBYRqwm7iEeIZ4lXicOE1+TSCQOyZLkTgohJZAySQtJa0jbSC2kU6Q+0hBpnEwm65Btyd7kCLKArCCXkbeQD5BPkvvJw+S3FDrFiOJMCaIkUqSUEko1ZT/lBKWfMkKZoKpRzame1AiqiDqfWkltoHZQL1OHqRM0dZolzZsWQ8ukLaPV0JppZ2n3aC/pdLoJ3YMeRZfQl9Jr6Afp5+mD9HcMDYYNg8dIYigZaxl7GacYtxkvmUymBdOXmchUMNcyG5lnmA+Yb1VYKvYqfBWRyhKVOpVWlX6V56pUVXNVP9V5qgtUq1UPq15WfaZGVbNQ46kJ1Bar1akdVbupNq7OUndSj1DPUV+jvl/9gvpjDbKGhUaghkijVGO3xhmNIRbGMmXxWELWclYD6yxrmE1iW7L57Ex2Bfsbdi97TFNDc6pmrGaRZp3mcc0BDsax4PA52ZxKziHODc57LQMtPy2x1mqtZq1+rTfaetq+2mLtcu0W7eva73VwnUCdLJ31Om0693UJuja6UbqFutt1z+o+02PreekJ9cr1Dund0Uf1bfSj9Rfq79bv0R83MDQINpAZbDE4Y/DMkGPoa5hpuNHwhOGoEctoupHEaKPRSaMnuCbuh2fjNXgXPmasbxxirDTeZdxrPGFiaTLbpMSkxeS+Kc2Ua5pmutG003TMzMgs3KzYrMnsjjnVnGueYb7ZvNv8jYWlRZzFSos2i8eW2pZ8ywWWTZb3rJhWPlZ5VvVW16xJ1lzrLOtt1ldsUBtXmwybOpvLtqitm63Edptt3xTiFI8p0in1U27aMez87ArsmuwG7Tn2YfYl9m32zx3MHBId1jt0O3xydHXMdmxwvOuk4TTDqcSpw+lXZxtnoXOd8zUXpkuQyxKXdpcXU22niqdun3rLleUa7rrStdP1o5u7m9yt2W3U3cw9xX2r+00umxvJXcM970H08PdY4nHM452nm6fC85DnL152Xlle+70eT7OcJp7WMG3I28Rb4L3Le2A6Pj1l+s7pAz7GPgKfep+Hvqa+It89viN+1n6Zfgf8nvs7+sv9j/i/4XnyFvFOBWABwQHlAb2BGoGzA2sDHwSZBKUHNQWNBbsGLww+FUIMCQ1ZH3KTb8AX8hv5YzPcZyya0RXKCJ0VWhv6MMwmTB7WEY6GzwjfEH5vpvlM6cy2CIjgR2yIuB9pGZkX+X0UKSoyqi7qUbRTdHF09yzWrORZ+2e9jvGPqYy5O9tqtnJ2Z6xqbFJsY+ybuIC4qriBeIf4RfGXEnQTJAntieTE2MQ9ieNzAudsmjOc5JpUlnRjruXcorkX5unOy553PFk1WZB8OIWYEpeyP+WDIEJQLxhP5aduTR0T8oSbhU9FvqKNolGxt7hKPJLmnVaV9jjdO31D+miGT0Z1xjMJT1IreZEZkrkj801WRNberM/ZcdktOZSclJyjUg1plrQr1zC3KLdPZisrkw3keeZtyhuTh8r35CP5c/PbFWyFTNGjtFKuUA4WTC+oK3hbGFt4uEi9SFrUM99m/ur5IwuCFny9kLBQuLCz2Lh4WfHgIr9FuxYji1MXdy4xXVK6ZHhp8NJ9y2jLspb9UOJYUlXyannc8o5Sg9KlpUMrglc0lamUycturvRauWMVYZVkVe9ql9VbVn8qF5VfrHCsqK74sEa45uJXTl/VfPV5bdra3kq3yu3rSOuk626s91m/r0q9akHV0IbwDa0b8Y3lG19tSt50oXpq9Y7NtM3KzQM1YTXtW8y2rNvyoTaj9nqdf13LVv2tq7e+2Sba1r/dd3vzDoMdFTve75TsvLUreFdrvUV99W7S7oLdjxpiG7q/5n7duEd3T8Wej3ulewf2Re/ranRvbNyvv7+yCW1SNo0eSDpw5ZuAb9qb7Zp3tXBaKg7CQeXBJ9+mfHvjUOihzsPcw83fmX+39QjrSHkr0jq/dawto22gPaG97+iMo50dXh1Hvrf/fu8x42N1xzWPV56gnSg98fnkgpPjp2Snnp1OPz3Umdx590z8mWtdUV29Z0PPnj8XdO5Mt1/3yfPe549d8Lxw9CL3Ytslt0utPa49R35w/eFIr1tv62X3y+1XPK509E3rO9Hv03/6asDVc9f41y5dn3m978bsG7duJt0cuCW69fh29u0XdwruTNxdeo94r/y+2v3qB/oP6n+0/rFlwG3g+GDAYM/DWQ/vDgmHnv6U/9OH4dJHzEfVI0YjjY+dHx8bDRq98mTOk+GnsqcTz8p+Vv9563Or59/94vtLz1j82PAL+YvPv655qfNy76uprzrHI8cfvM55PfGm/K3O233vuO+638e9H5ko/ED+UPPR+mPHp9BP9z7nfP78L/eE8/sl0p8zAAA6NGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxMTEgNzkuMTU4MzI1LCAyMDE1LzA5LzEwLTAxOjEwOjIwICAgICAgICAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgICAgICAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgICAgICAgICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ0MgMjAxNSAoTWFjaW50b3NoKTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOkNyZWF0ZURhdGU+MjAxNi0wMi0wOFQxMzoyNjo1MSswOTowMDwveG1wOkNyZWF0ZURhdGU+CiAgICAgICAgIDx4bXA6TWV0YWRhdGFEYXRlPjIwMTYtMDItMDhUMTM6MjY6NTErMDk6MDA8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgICAgIDx4bXA6TW9kaWZ5RGF0ZT4yMDE2LTAyLTA4VDEzOjI2OjUxKzA5OjAwPC94bXA6TW9kaWZ5RGF0ZT4KICAgICAgICAgPHhtcE1NOkluc3RhbmNlSUQ+eG1wLmlpZDplN2QyMjJjOS03Nzg2LTRhMjktOTlhMy0yYjRjNWI4YTA5MWY8L3htcE1NOkluc3RhbmNlSUQ+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPmFkb2JlOmRvY2lkOnBob3Rvc2hvcDo5OWE3ZTJmOC0wZWFhLTExNzktYTE4NS05M2Y1MjZhZGU2ZDg8L3htcE1NOkRvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ+eG1wLmRpZDoyYmY3M2M3Ny03NzczLTQzNjAtYThiZS1kY2UyZTgzMWQ2YmM8L3htcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOkhpc3Rvcnk+CiAgICAgICAgICAgIDxyZGY6U2VxPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5jcmVhdGVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6MmJmNzNjNzctNzc3My00MzYwLWE4YmUtZGNlMmU4MzFkNmJjPC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE2LTAyLTA4VDEzOjI2OjUxKzA5OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ0MgMjAxNSAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6ZTdkMjIyYzktNzc4Ni00YTI5LTk5YTMtMmI0YzViOGEwOTFmPC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE2LTAyLTA4VDEzOjI2OjUxKzA5OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ0MgMjAxNSAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC94bXBNTTpIaXN0b3J5PgogICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3BuZzwvZGM6Zm9ybWF0PgogICAgICAgICA8cGhvdG9zaG9wOkNvbG9yTW9kZT4zPC9waG90b3Nob3A6Q29sb3JNb2RlPgogICAgICAgICA8cGhvdG9zaG9wOklDQ1Byb2ZpbGU+c1JHQiBJRUM2MTk2Ni0yLjE8L3Bob3Rvc2hvcDpJQ0NQcm9maWxlPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8dGlmZjpYUmVzb2x1dGlvbj43MjAwMDAvMTAwMDA8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjcyMDAwMC8xMDAwMDwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4xPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjE8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVuZD0idyI/PoAp5ZwAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAABNJREFUeNpi+P//PwMAAAD//wMACPwC/tOldWEAAAAASUVORK5CYII='> +</body> +</html> diff --git a/dom/security/test/csp/file_data-uri_blocked.html^headers^ b/dom/security/test/csp/file_data-uri_blocked.html^headers^ new file mode 100644 index 000000000..f593253f9 --- /dev/null +++ b/dom/security/test/csp/file_data-uri_blocked.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self'; img-src 'none' diff --git a/dom/security/test/csp/file_doccomment_meta.html b/dom/security/test/csp/file_doccomment_meta.html new file mode 100644 index 000000000..a0f36a4bf --- /dev/null +++ b/dom/security/test/csp/file_doccomment_meta.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 663570 - Test doc.write(meta csp)</title> + <meta charset="utf-8"> + + <!-- Use doc.write() to *un*apply meta csp --> + <script type="application/javascript"> + document.write("<!--"); + </script> + + <meta http-equiv="Content-Security-Policy" content= "style-src 'none'; script-src 'none'; img-src 'none'"> + --> + + <!-- try to load a css on a page where meta CSP is commented out --> + <link rel="stylesheet" type="text/css" href="file_docwrite_meta.css"> + + <!-- try to load a script on a page where meta CSP is commented out --> + <script id="testscript" src="file_docwrite_meta.js"></script> + +</head> +<body> + + <!-- try to load an image on a page where meta CSP is commented out --> + <img id="testimage" src="http://mochi.test:8888/tests/image/test/mochitest/blue.png"></img> + +</body> +</html> diff --git a/dom/security/test/csp/file_docwrite_meta.css b/dom/security/test/csp/file_docwrite_meta.css new file mode 100644 index 000000000..de725038b --- /dev/null +++ b/dom/security/test/csp/file_docwrite_meta.css @@ -0,0 +1,3 @@ +body { + background-color: rgb(255, 0, 0); +} diff --git a/dom/security/test/csp/file_docwrite_meta.html b/dom/security/test/csp/file_docwrite_meta.html new file mode 100644 index 000000000..292de3bec --- /dev/null +++ b/dom/security/test/csp/file_docwrite_meta.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 663570 - Test doc.write(meta csp)</title> + <meta charset="utf-8"> + + <!-- Use doc.write() to apply meta csp --> + <script type="application/javascript"> + var metaCSP = "style-src 'none'; script-src 'none'; img-src 'none'"; + document.write("<meta http-equiv=\"Content-Security-Policy\" content=\" " + metaCSP + "\">"); + </script> + + <!-- try to load a css which is forbidden by meta CSP --> + <link rel="stylesheet" type="text/css" href="file_docwrite_meta.css"> + + <!-- try to load a script which is forbidden by meta CSP --> + <script id="testscript" src="file_docwrite_meta.js"></script> + +</head> +<body> + + <!-- try to load an image which is forbidden by meta CSP --> + <img id="testimage" src="http://mochi.test:8888/tests/image/test/mochitest/blue.png"></img> + +</body> +</html> diff --git a/dom/security/test/csp/file_docwrite_meta.js b/dom/security/test/csp/file_docwrite_meta.js new file mode 100644 index 000000000..722adc235 --- /dev/null +++ b/dom/security/test/csp/file_docwrite_meta.js @@ -0,0 +1,3 @@ +// set a variable on the document which we can check to verify +// whether the external script was loaded or blocked +document.myMetaCSPScript = "external-JS-loaded"; diff --git a/dom/security/test/csp/file_dual_header_testserver.sjs b/dom/security/test/csp/file_dual_header_testserver.sjs new file mode 100644 index 000000000..d5631e783 --- /dev/null +++ b/dom/security/test/csp/file_dual_header_testserver.sjs @@ -0,0 +1,46 @@ +/* + * Custom sjs file serving a test page using *two* CSP policies. + * See Bug 1036399 - Multiple CSP policies should be combined towards an intersection + */ + +const TIGHT_POLICY = "default-src 'self'"; +const LOOSE_POLICY = "default-src 'self' 'unsafe-inline'"; + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + var csp = ""; + // deliver *TWO* comma separated policies which is in fact the same as serving + // to separate CSP headers (AppendPolicy is called twice). + if (request.queryString == "tight") { + // script execution will be *blocked* + csp = TIGHT_POLICY + ", " + LOOSE_POLICY; + } + else { + // script execution will be *allowed* + csp = LOOSE_POLICY + ", " + LOOSE_POLICY; + } + response.setHeader("Content-Security-Policy", csp, false); + + // Send HTML to test allowed/blocked behaviors + response.setHeader("Content-Type", "text/html", false); + + // generate an html file that contains a div container which is updated + // in case the inline script is *not* blocked by CSP. + var html = "<!DOCTYPE HTML>" + + "<html>" + + "<head>" + + "<title>Testpage for Bug 1036399</title>" + + "</head>" + + "<body>" + + "<div id='testdiv'>blocked</div>" + + "<script type='text/javascript'>" + + "document.getElementById('testdiv').innerHTML = 'allowed';" + + "</script>" + + "</body>" + + "</html>"; + + response.write(html); +} diff --git a/dom/security/test/csp/file_evalscript_main.html b/dom/security/test/csp/file_evalscript_main.html new file mode 100644 index 000000000..e83c1d9ed --- /dev/null +++ b/dom/security/test/csp/file_evalscript_main.html @@ -0,0 +1,12 @@ +<html> + <head> + <title>CSP eval script tests</title> + <script type="application/javascript" + src="file_evalscript_main.js"></script> + </head> + <body> + + Foo. + + </body> +</html> diff --git a/dom/security/test/csp/file_evalscript_main.html^headers^ b/dom/security/test/csp/file_evalscript_main.html^headers^ new file mode 100644 index 000000000..b91ba384d --- /dev/null +++ b/dom/security/test/csp/file_evalscript_main.html^headers^ @@ -0,0 +1,2 @@ +Cache-Control: no-cache +Content-Security-Policy: default-src 'self' diff --git a/dom/security/test/csp/file_evalscript_main.js b/dom/security/test/csp/file_evalscript_main.js new file mode 100644 index 000000000..64ab05664 --- /dev/null +++ b/dom/security/test/csp/file_evalscript_main.js @@ -0,0 +1,154 @@ +// some javascript for the CSP eval() tests + +function logResult(str, passed) { + var elt = document.createElement('div'); + var color = passed ? "#cfc;" : "#fcc"; + elt.setAttribute('style', 'background-color:' + color + '; width:100%; border:1px solid black; padding:3px; margin:4px;'); + elt.innerHTML = str; + document.body.appendChild(elt); +} + +window._testResults = {}; + +// check values for return values from blocked timeout or intervals +var verifyZeroRetVal = (function(window) { + return function(val, details) { + logResult((val === 0 ? "PASS: " : "FAIL: ") + "Blocked interval/timeout should have zero return value; " + details, val === 0); + window.parent.verifyZeroRetVal(val, details); + };})(window); + +// callback for when stuff is allowed by CSP +var onevalexecuted = (function(window) { + return function(shouldrun, what, data) { + window._testResults[what] = "ran"; + window.parent.scriptRan(shouldrun, what, data); + logResult((shouldrun ? "PASS: " : "FAIL: ") + what + " : " + data, shouldrun); + };})(window); + +// callback for when stuff is blocked +var onevalblocked = (function(window) { + return function(shouldrun, what, data) { + window._testResults[what] = "blocked"; + window.parent.scriptBlocked(shouldrun, what, data); + logResult((shouldrun ? "FAIL: " : "PASS: ") + what + " : " + data, !shouldrun); + };})(window); + + +// Defer until document is loaded so that we can write the pretty result boxes +// out. +addEventListener('load', function() { + // setTimeout(String) test -- mutate something in the window._testResults + // obj, then check it. + { + var str_setTimeoutWithStringRan = 'onevalexecuted(false, "setTimeout(String)", "setTimeout with a string was enabled.");'; + function fcn_setTimeoutWithStringCheck() { + if (this._testResults["setTimeout(String)"] !== "ran") { + onevalblocked(false, "setTimeout(String)", + "setTimeout with a string was blocked"); + } + } + setTimeout(fcn_setTimeoutWithStringCheck.bind(window), 10); + var res = setTimeout(str_setTimeoutWithStringRan, 10); + verifyZeroRetVal(res, "setTimeout(String)"); + } + + // setInterval(String) test -- mutate something in the window._testResults + // obj, then check it. + { + var str_setIntervalWithStringRan = 'onevalexecuted(false, "setInterval(String)", "setInterval with a string was enabled.");'; + function fcn_setIntervalWithStringCheck() { + if (this._testResults["setInterval(String)"] !== "ran") { + onevalblocked(false, "setInterval(String)", + "setInterval with a string was blocked"); + } + } + setTimeout(fcn_setIntervalWithStringCheck.bind(window), 10); + var res = setInterval(str_setIntervalWithStringRan, 10); + verifyZeroRetVal(res, "setInterval(String)"); + + // emergency cleanup, just in case. + if (res != 0) { + setTimeout(function () { clearInterval(res); }, 15); + } + } + + // setTimeout(function) test -- mutate something in the window._testResults + // obj, then check it. + { + function fcn_setTimeoutWithFunctionRan() { + onevalexecuted(true, "setTimeout(function)", + "setTimeout with a function was enabled.") + } + function fcn_setTimeoutWithFunctionCheck() { + if (this._testResults["setTimeout(function)"] !== "ran") { + onevalblocked(true, "setTimeout(function)", + "setTimeout with a function was blocked"); + } + } + setTimeout(fcn_setTimeoutWithFunctionRan.bind(window), 10); + setTimeout(fcn_setTimeoutWithFunctionCheck.bind(window), 10); + } + + // eval() test -- should throw exception as per spec + try { + eval('onevalexecuted(false, "eval(String)", "eval() was enabled.");'); + } catch (e) { + onevalblocked(false, "eval(String)", + "eval() was blocked"); + } + + // eval(foo,bar) test -- should throw exception as per spec + try { + eval('onevalexecuted(false, "eval(String,scope)", "eval() was enabled.");',1); + } catch (e) { + onevalblocked(false, "eval(String,object)", + "eval() with scope was blocked"); + } + + // [foo,bar].sort(eval) test -- should throw exception as per spec + try { + ['onevalexecuted(false, "[String, obj].sort(eval)", "eval() was enabled.");',1].sort(eval); + } catch (e) { + onevalblocked(false, "[String, obj].sort(eval)", + "eval() with scope via sort was blocked"); + } + + // [].sort.call([foo,bar], eval) test -- should throw exception as per spec + try { + [].sort.call(['onevalexecuted(false, "[String, obj].sort(eval)", "eval() was enabled.");',1], eval); + } catch (e) { + onevalblocked(false, "[].sort.call([String, obj], eval)", + "eval() with scope via sort/call was blocked"); + } + + // new Function() test -- should throw exception as per spec + try { + var fcn = new Function('onevalexecuted(false, "new Function(String)", "new Function(String) was enabled.");'); + fcn(); + } catch (e) { + onevalblocked(false, "new Function(String)", + "new Function(String) was blocked."); + } + + // setTimeout(eval, 0, str) + { + // error is not catchable here, instead, we're going to side-effect + // 'worked'. + var worked = false; + + setTimeout(eval, 0, 'worked = true'); + setTimeout(function(worked) { + if (worked) { + onevalexecuted(false, "setTimeout(eval, 0, str)", + "setTimeout(eval, 0, string) was enabled."); + } else { + onevalblocked(false, "setTimeout(eval, 0, str)", + "setTimeout(eval, 0, str) was blocked."); + } + }, 0, worked); + } + +}, false); + + + diff --git a/dom/security/test/csp/file_evalscript_main_allowed.html b/dom/security/test/csp/file_evalscript_main_allowed.html new file mode 100644 index 000000000..274972d9b --- /dev/null +++ b/dom/security/test/csp/file_evalscript_main_allowed.html @@ -0,0 +1,12 @@ +<html> + <head> + <title>CSP eval script tests</title> + <script type="application/javascript" + src="file_evalscript_main_allowed.js"></script> + </head> + <body> + + Foo. + + </body> +</html> diff --git a/dom/security/test/csp/file_evalscript_main_allowed.html^headers^ b/dom/security/test/csp/file_evalscript_main_allowed.html^headers^ new file mode 100644 index 000000000..0cb5288be --- /dev/null +++ b/dom/security/test/csp/file_evalscript_main_allowed.html^headers^ @@ -0,0 +1,2 @@ +Cache-Control: no-cache +Content-Security-Policy: default-src 'self' ; script-src 'self' 'unsafe-eval' diff --git a/dom/security/test/csp/file_evalscript_main_allowed.js b/dom/security/test/csp/file_evalscript_main_allowed.js new file mode 100644 index 000000000..69e6f7a4f --- /dev/null +++ b/dom/security/test/csp/file_evalscript_main_allowed.js @@ -0,0 +1,121 @@ +// some javascript for the CSP eval() tests +// all of these evals should succeed, as the document loading this script +// has script-src 'self' 'unsafe-eval' + +function logResult(str, passed) { + var elt = document.createElement('div'); + var color = passed ? "#cfc;" : "#fcc"; + elt.setAttribute('style', 'background-color:' + color + '; width:100%; border:1px solid black; padding:3px; margin:4px;'); + elt.innerHTML = str; + document.body.appendChild(elt); +} + +// callback for when stuff is allowed by CSP +var onevalexecuted = (function(window) { + return function(shouldrun, what, data) { + window.parent.scriptRan(shouldrun, what, data); + logResult((shouldrun ? "PASS: " : "FAIL: ") + what + " : " + data, shouldrun); + };})(window); + +// callback for when stuff is blocked +var onevalblocked = (function(window) { + return function(shouldrun, what, data) { + window.parent.scriptBlocked(shouldrun, what, data); + logResult((shouldrun ? "FAIL: " : "PASS: ") + what + " : " + data, !shouldrun); + };})(window); + + +// Defer until document is loaded so that we can write the pretty result boxes +// out. +addEventListener('load', function() { + // setTimeout(String) test -- should pass + try { + setTimeout('onevalexecuted(true, "setTimeout(String)", "setTimeout with a string was enabled.");', 10); + } catch (e) { + onevalblocked(true, "setTimeout(String)", + "setTimeout with a string was blocked"); + } + + // setTimeout(function) test -- should pass + try { + setTimeout(function() { + onevalexecuted(true, "setTimeout(function)", + "setTimeout with a function was enabled.") + }, 10); + } catch (e) { + onevalblocked(true, "setTimeout(function)", + "setTimeout with a function was blocked"); + } + + // eval() test + try { + eval('onevalexecuted(true, "eval(String)", "eval() was enabled.");'); + } catch (e) { + onevalblocked(true, "eval(String)", + "eval() was blocked"); + } + + // eval(foo,bar) test + try { + eval('onevalexecuted(true, "eval(String,scope)", "eval() was enabled.");',1); + } catch (e) { + onevalblocked(true, "eval(String,object)", + "eval() with scope was blocked"); + } + + // [foo,bar].sort(eval) test + try { + ['onevalexecuted(true, "[String, obj].sort(eval)", "eval() was enabled.");',1].sort(eval); + } catch (e) { + onevalblocked(true, "[String, obj].sort(eval)", + "eval() with scope via sort was blocked"); + } + + // [].sort.call([foo,bar], eval) test + try { + [].sort.call(['onevalexecuted(true, "[String, obj].sort(eval)", "eval() was enabled.");',1], eval); + } catch (e) { + onevalblocked(true, "[].sort.call([String, obj], eval)", + "eval() with scope via sort/call was blocked"); + } + + // new Function() test + try { + var fcn = new Function('onevalexecuted(true, "new Function(String)", "new Function(String) was enabled.");'); + fcn(); + } catch (e) { + onevalblocked(true, "new Function(String)", + "new Function(String) was blocked."); + } + + function checkResult() { + //alert(bar); + if (bar) { + onevalexecuted(true, "setTimeout(eval, 0, str)", + "setTimeout(eval, 0, string) was enabled."); + } else { + onevalblocked(true, "setTimeout(eval, 0, str)", + "setTimeout(eval, 0, str) was blocked."); + } + } + + var bar = false; + + function foo() { + bar = true; + } + + window.foo = foo; + + // setTimeout(eval, 0, str) + + // error is not catchable here + + setTimeout(eval, 0, 'window.foo();'); + + setTimeout(checkResult.bind(this), 0); + +}, false); + + + diff --git a/dom/security/test/csp/file_fontloader.sjs b/dom/security/test/csp/file_fontloader.sjs new file mode 100644 index 000000000..06f6e752d --- /dev/null +++ b/dom/security/test/csp/file_fontloader.sjs @@ -0,0 +1,58 @@ +// custom *.sjs for Bug 1195172 +// CSP: 'block-all-mixed-content' + +const PRE_HEAD = + "<!DOCTYPE HTML>" + + "<html><head><meta charset=\"utf-8\">" + + "<title>Bug 1195172 - CSP should block font from cache</title>"; + +const CSP_BLOCK = + "<meta http-equiv=\"Content-Security-Policy\" content=\"font-src 'none'\">"; + +const CSP_ALLOW = + "<meta http-equiv=\"Content-Security-Policy\" content=\"font-src *\">"; + +const CSS = + "<style>" + + " @font-face {" + + " font-family: myFontTest;" + + " src: url(file_fontloader.woff);" + + " }" + + " div {" + + " font-family: myFontTest;" + + " }" + + "</style>"; + +const POST_HEAD_AND_BODY = + "</head>" + + "<body>" + + "<div> Just testing the font </div>" + + "</body>" + + "</html>"; + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + var queryString = request.queryString; + + if (queryString == "baseline") { + response.write(PRE_HEAD + POST_HEAD_AND_BODY); + return; + } + if (queryString == "no-csp") { + response.write(PRE_HEAD + CSS + POST_HEAD_AND_BODY); + return; + } + if (queryString == "csp-block") { + response.write(PRE_HEAD + CSP_BLOCK + CSS + POST_HEAD_AND_BODY); + return; + } + if (queryString == "csp-allow") { + response.write(PRE_HEAD + CSP_ALLOW + CSS + POST_HEAD_AND_BODY); + return; + } + // we should never get here, but just in case return something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/csp/file_fontloader.woff b/dom/security/test/csp/file_fontloader.woff Binary files differnew file mode 100644 index 000000000..fbf7390d5 --- /dev/null +++ b/dom/security/test/csp/file_fontloader.woff diff --git a/dom/security/test/csp/file_form-action.html b/dom/security/test/csp/file_form-action.html new file mode 100644 index 000000000..cfff156ba --- /dev/null +++ b/dom/security/test/csp/file_form-action.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 529697 - Test mapping of form submission to form-action</title> +</head> +<body> + <form action="submit-form"> + <input id="submitButton" type="submit" value="Submit form"> + </form> + <script type="text/javascript"> + var submitButton = document.getElementById('submitButton'); + submitButton.click(); + </script> +</body> +</html> diff --git a/dom/security/test/csp/file_form_action_server.sjs b/dom/security/test/csp/file_form_action_server.sjs new file mode 100644 index 000000000..f2771d898 --- /dev/null +++ b/dom/security/test/csp/file_form_action_server.sjs @@ -0,0 +1,33 @@ +// Custom *.sjs file specifically for the needs of Bug 1251043 + +const FRAME = ` + <!DOCTYPE html> + <html> + <head> + <title>Bug 1251043 - Test form-action blocks URL</title> + <meta http-equiv="Content-Security-Policy" content="form-action 'none';"> + </head> + <body> + CONTROL-TEXT + <form action="file_form_action_server.sjs?formsubmission" method="GET"> + <input type="submit" id="submitButton" value="submit"> + </form> + </body> + </html>`; + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // PART 1: Return a frame including the FORM and the CSP + if (request.queryString === "loadframe") { + response.write(FRAME); + return; + } + + // PART 2: We should never get here because the form + // should not be submitted. Just in case; return + // something unexpected so the test fails! + response.write("do'h"); +} diff --git a/dom/security/test/csp/file_frameancestors.sjs b/dom/security/test/csp/file_frameancestors.sjs new file mode 100644 index 000000000..d0a77893f --- /dev/null +++ b/dom/security/test/csp/file_frameancestors.sjs @@ -0,0 +1,54 @@ +// SJS file for CSP frame ancestor mochitests +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + var isPreflight = request.method == "OPTIONS"; + + + //avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // grab the desired policy from the query, and then serve a page + if (query['csp']) + response.setHeader("Content-Security-Policy", + unescape(query['csp']), + false); + if (query['scriptedreport']) { + // spit back a script that records that the page loaded + response.setHeader("Content-Type", "text/javascript", false); + if (query['double']) + response.write('window.parent.parent.parent.postMessage({call: "frameLoaded", testname: "' + query['scriptedreport'] + '", uri: "window.location.toString()"}, "*");'); + else + response.write('window.parent.parent.postMessage({call: "frameLoaded", testname: "' + query['scriptedreport'] + '", uri: "window.location.toString()"}, "*");'); + } else if (query['internalframe']) { + // spit back an internal iframe (one that might be blocked) + response.setHeader("Content-Type", "text/html", false); + response.write('<html><head>'); + if (query['double']) + response.write('<script src="file_frameancestors.sjs?double=1&scriptedreport=' + query['testid'] + '"></script>'); + else + response.write('<script src="file_frameancestors.sjs?scriptedreport=' + query['testid'] + '"></script>'); + response.write('</head><body>'); + response.write(unescape(query['internalframe'])); + response.write('</body></html>'); + } else if (query['externalframe']) { + // spit back an internal iframe (one that won't be blocked, and probably + // has no CSP) + response.setHeader("Content-Type", "text/html", false); + response.write('<html><head>'); + response.write('</head><body>'); + response.write(unescape(query['externalframe'])); + response.write('</body></html>'); + } else { + // default case: error. + response.setHeader("Content-Type", "text/html", false); + response.write('<html><body>'); + response.write("ERROR: not sure what to serve."); + response.write('</body></html>'); + } +} diff --git a/dom/security/test/csp/file_frameancestors_main.html b/dom/security/test/csp/file_frameancestors_main.html new file mode 100644 index 000000000..97f9cb9ac --- /dev/null +++ b/dom/security/test/csp/file_frameancestors_main.html @@ -0,0 +1,44 @@ +<html> + <head> + <title>CSP frame ancestors tests</title> + + <!-- this page shouldn't have a CSP, just the sub-pages. --> + <script src='file_frameancestors_main.js'></script> + + </head> + <body> + +<!-- These iframes will get populated by the attached javascript. --> +<tt> aa_allow: /* innermost frame allows a */</tt><br/> +<iframe id='aa_allow'></iframe><br/> + +<tt> aa_block: /* innermost frame denies a */</tt><br/> +<iframe id='aa_block'></iframe><br/> + +<tt> ab_allow: /* innermost frame allows a */</tt><br/> +<iframe id='ab_allow'></iframe><br/> + +<tt> ab_block: /* innermost frame denies a */</tt><br/> +<iframe id='ab_block'></iframe><br/> + +<tt> aba_allow: /* innermost frame allows b,a */</tt><br/> +<iframe id='aba_allow'></iframe><br/> + +<tt> aba_block: /* innermost frame denies b */</tt><br/> +<iframe id='aba_block'></iframe><br/> + +<tt> aba2_block: /* innermost frame denies a */</tt><br/> +<iframe id='aba2_block'></iframe><br/> + +<tt> abb_allow: /* innermost frame allows b,a */</tt><br/> +<iframe id='abb_allow'></iframe><br/> + +<tt> abb_block: /* innermost frame denies b */</tt><br/> +<iframe id='abb_block'></iframe><br/> + +<tt> abb2_block: /* innermost frame denies a */</tt><br/> +<iframe id='abb2_block'></iframe><br/> + + + </body> +</html> diff --git a/dom/security/test/csp/file_frameancestors_main.js b/dom/security/test/csp/file_frameancestors_main.js new file mode 100644 index 000000000..caffc7257 --- /dev/null +++ b/dom/security/test/csp/file_frameancestors_main.js @@ -0,0 +1,65 @@ +// Script to populate the test frames in the frame ancestors mochitest. +// +function setupFrames() { + + var $ = function(v) { return document.getElementById(v); } + var base = { + self: '/tests/dom/security/test/csp/file_frameancestors.sjs', + a: 'http://mochi.test:8888/tests/dom/security/test/csp/file_frameancestors.sjs', + b: 'http://example.com/tests/dom/security/test/csp/file_frameancestors.sjs' + }; + + var host = { a: 'http://mochi.test:8888', b: 'http://example.com:80' }; + + var innerframeuri = null; + var elt = null; + + elt = $('aa_allow'); + elt.src = base.a + "?testid=aa_allow&internalframe=aa_a&csp=" + + escape("default-src 'none'; frame-ancestors " + host.a + "; script-src 'self'"); + + elt = $('aa_block'); + elt.src = base.a + "?testid=aa_block&internalframe=aa_b&csp=" + + escape("default-src 'none'; frame-ancestors 'none'; script-src 'self'"); + + elt = $('ab_allow'); + elt.src = base.b + "?testid=ab_allow&internalframe=ab_a&csp=" + + escape("default-src 'none'; frame-ancestors " + host.a + "; script-src 'self'"); + + elt = $('ab_block'); + elt.src = base.b + "?testid=ab_block&internalframe=ab_b&csp=" + + escape("default-src 'none'; frame-ancestors 'none'; script-src 'self'"); + + /* .... two-level framing */ + elt = $('aba_allow'); + innerframeuri = base.a + "?testid=aba_allow&double=1&internalframe=aba_a&csp=" + + escape("default-src 'none'; frame-ancestors " + host.a + " " + host.b + "; script-src 'self'"); + elt.src = base.b + "?externalframe=" + escape('<iframe src="' + innerframeuri + '"></iframe>'); + + elt = $('aba_block'); + innerframeuri = base.a + "?testid=aba_allow&double=1&internalframe=aba_b&csp=" + + escape("default-src 'none'; frame-ancestors " + host.a + "; script-src 'self'"); + elt.src = base.b + "?externalframe=" + escape('<iframe src="' + innerframeuri + '"></iframe>'); + + elt = $('aba2_block'); + innerframeuri = base.a + "?testid=aba_allow&double=1&internalframe=aba2_b&csp=" + + escape("default-src 'none'; frame-ancestors " + host.b + "; script-src 'self'"); + elt.src = base.b + "?externalframe=" + escape('<iframe src="' + innerframeuri + '"></iframe>'); + + elt = $('abb_allow'); + innerframeuri = base.b + "?testid=abb_allow&double=1&internalframe=abb_a&csp=" + + escape("default-src 'none'; frame-ancestors " + host.a + " " + host.b + "; script-src 'self'"); + elt.src = base.b + "?externalframe=" + escape('<iframe src="' + innerframeuri + '"></iframe>'); + + elt = $('abb_block'); + innerframeuri = base.b + "?testid=abb_allow&double=1&internalframe=abb_b&csp=" + + escape("default-src 'none'; frame-ancestors " + host.a + "; script-src 'self'"); + elt.src = base.b + "?externalframe=" + escape('<iframe src="' + innerframeuri + '"></iframe>'); + + elt = $('abb2_block'); + innerframeuri = base.b + "?testid=abb_allow&double=1&internalframe=abb2_b&csp=" + + escape("default-src 'none'; frame-ancestors " + host.b + "; script-src 'self'"); + elt.src = base.b + "?externalframe=" + escape('<iframe src="' + innerframeuri + '"></iframe>'); +} + +window.addEventListener('load', setupFrames, false); diff --git a/dom/security/test/csp/file_hash_source.html b/dom/security/test/csp/file_hash_source.html new file mode 100644 index 000000000..47eba6cf3 --- /dev/null +++ b/dom/security/test/csp/file_hash_source.html @@ -0,0 +1,65 @@ +<!doctype html> +<html> + <body> + <!-- inline scripts --> + <p id="inline-script-valid-hash">blocked</p> + <p id="inline-script-invalid-hash">blocked</p> + <p id="inline-script-invalid-hash-valid-nonce">blocked</p> + <p id="inline-script-valid-hash-invalid-nonce">blocked</p> + <p id="inline-script-invalid-hash-invalid-nonce">blocked</p> + <p id="inline-script-valid-sha512-hash">blocked</p> + <p id="inline-script-valid-sha384-hash">blocked</p> + <p id="inline-script-valid-sha1-hash">blocked</p> + <p id="inline-script-valid-md5-hash">blocked</p> + + <!-- 'sha256-siVR8vAcqP06h2ppeNwqgjr0yZ6yned4X2VF84j4GmI=' (in policy) --> + <script>document.getElementById("inline-script-valid-hash").innerHTML = "allowed";</script> + <!-- 'sha256-cYPTF2pm0QeyDtbmJ3+xi00o2Rxrw7vphBoHgOg9EnQ=' (not in policy) --> + <script>document.getElementById("inline-script-invalid-hash").innerHTML = "allowed";</script> + <!-- 'sha256-SKtBKyfeMjBpOujES0etR9t/cklbouJu/3T4PXnjbIo=' (not in policy) --> + <script nonce="jPRxvuRHbiQnCWVuoCMAvQ==">document.getElementById("inline-script-invalid-hash-valid-nonce").innerHTML = "allowed";</script> + <!-- 'sha256-z7rzCkbOJqi08lga3CVQ3b+3948ZbJWaSxsBs8zPliE=' --> + <script nonce="foobar">document.getElementById("inline-script-valid-hash-invalid-nonce").innerHTML = "allowed";</script> + <!-- 'sha256-E5TX2PmYZ4YQOK/F3XR1wFcvFjbO7QHMmxHTT/18LbE=' (not in policy) --> + <script nonce="foobar">document.getElementById("inline-script-invalid-hash-invalid-nonce").innerHTML = "allowed";</script> + <!-- 'sha512-tMLuv22jJ5RHkvLNlv0otvA2fgw6PF16HKu6wy0ZDQ3M7UKzoygs1uxIMSfjMttgWrB5WRvIr35zrTZppMYBVw==' (in policy) --> + <script>document.getElementById("inline-script-valid-sha512-hash").innerHTML = "allowed";</script> + <!-- 'sha384-XjAD+FxZfipkxna4id1JrR2QP6OYUZfAxpn9+yHOmT1VSLVa9SQR/dz7CEb7jw7w' (in policy) --> + <script>document.getElementById("inline-script-valid-sha384-hash").innerHTML = "allowed";</script> + <!-- 'sha1-LHErkMxKGcSpa/znpzmKYkKnI30=' (in policy) --> + <script>document.getElementById("inline-script-valid-sha1-hash").innerHTML = "allowed";</script> + <!-- 'md5-/m4wX3YU+IHs158KwKOBWg==' (in policy) --> + <script>document.getElementById("inline-script-valid-md5-hash").innerHTML = "allowed";</script> + + <!-- inline styles --> + <p id="inline-style-valid-hash"></p> + <p id="inline-style-invalid-hash"></p> + <p id="inline-style-invalid-hash-valid-nonce"></p> + <p id="inline-style-valid-hash-invalid-nonce"></p> + <p id="inline-style-invalid-hash-invalid-nonce"></p> + <p id="inline-style-valid-sha512-hash"></p> + <p id="inline-style-valid-sha384-hash"></p> + <p id="inline-style-valid-sha1-hash"></p> + <p id="inline-style-valid-md5-hash"></p> + + <!-- 'sha256-UpNH6x+Ux99QTW1fJikQsVbBERJruIC98et0YDVKKHQ=' (in policy) --> + <style>p#inline-style-valid-hash { color: green; }</style> + <!-- 'sha256-+TYxTx+bsfTDdivWLZUwScEYyxuv6lknMbNjrgGBRZo=' (not in policy) --> + <style>p#inline-style-invalid-hash { color: red; }</style> + <!-- 'sha256-U+9UPC/CFzz3QuOrl5q3KCVNngOYWuIkE2jK6Ir0Mbs=' (not in policy) --> + <style nonce="ftL2UbGHlSEaZTLWMwtA5Q==">p#inline-style-invalid-hash-valid-nonce { color: green; }</style> + <!-- 'sha256-0IPbWW5IDJ/juvETq60oTnhC+XzOqdYp5/UBsBKCaOY=' (in policy) --> + <style nonce="foobar">p#inline-style-valid-hash-invalid-nonce { color: green; }</style> + <!-- 'sha256-KaHZgPd4nC4S8BVLT/9WjzdPDtunGWojR83C2whbd50=' (not in policy) --> + <style nonce="foobar">p#inline-style-invalid-hash-invalid-nonce { color: red; }</style> + <!-- 'sha512-EpcDbSuvFv0HIyKtU5tQMN7UtBMeEbljz1dWPfy7PNCa1RYdHKwdJWT1tie41evq/ZUL1rzadSVdEzq3jl6Twg==' (in policy) --> + <style>p#inline-style-valid-sha512-hash { color: green; }</style> + <!-- 'sha384-c5W8ON4WyeA2zEOGdrOGhRmRYI8+2UzUUmhGQFjUFP6yiPZx9FGEV3UOiQ+tIshF' (in policy) --> + <style>p#inline-style-valid-sha384-hash { color: green; }</style> + <!-- 'sha1-T/+b4sxCIiJxDr6XS9dAEyHKt2M=' (in policy) --> + <style>p#inline-style-valid-sha1-hash { color: red; }</style> + <!-- 'md5-oNrgrtzOZduwDYYi1yo12g==' (in policy) --> + <style>p#inline-style-valid-md5-hash { color: red; }</style> + + </body> +</html> diff --git a/dom/security/test/csp/file_hash_source.html^headers^ b/dom/security/test/csp/file_hash_source.html^headers^ new file mode 100644 index 000000000..785d63391 --- /dev/null +++ b/dom/security/test/csp/file_hash_source.html^headers^ @@ -0,0 +1,2 @@ +Content-Security-Policy: script-src 'sha256-siVR8vAcqP06h2ppeNwqgjr0yZ6yned4X2VF84j4GmI=' 'nonce-jPRxvuRHbiQnCWVuoCMAvQ==' 'sha256-z7rzCkbOJqi08lga3CVQ3b+3948ZbJWaSxsBs8zPliE=' 'sha512-tMLuv22jJ5RHkvLNlv0otvA2fgw6PF16HKu6wy0ZDQ3M7UKzoygs1uxIMSfjMttgWrB5WRvIr35zrTZppMYBVw==' 'sha384-XjAD+FxZfipkxna4id1JrR2QP6OYUZfAxpn9+yHOmT1VSLVa9SQR/dz7CEb7jw7w' 'sha1-LHErkMxKGcSpa/znpzmKYkKnI30=' 'md5-/m4wX3YU+IHs158KwKOBWg=='; style-src 'sha256-UpNH6x+Ux99QTW1fJikQsVbBERJruIC98et0YDVKKHQ=' 'nonce-ftL2UbGHlSEaZTLWMwtA5Q==' 'sha256-0IPbWW5IDJ/juvETq60oTnhC+XzOqdYp5/UBsBKCaOY=' 'sha512-EpcDbSuvFv0HIyKtU5tQMN7UtBMeEbljz1dWPfy7PNCa1RYdHKwdJWT1tie41evq/ZUL1rzadSVdEzq3jl6Twg==' 'sha384-c5W8ON4WyeA2zEOGdrOGhRmRYI8+2UzUUmhGQFjUFP6yiPZx9FGEV3UOiQ+tIshF' 'sha1-T/+b4sxCIiJxDr6XS9dAEyHKt2M=' 'md5-oNrgrtzOZduwDYYi1yo12g=='; +Cache-Control: no-cache diff --git a/dom/security/test/csp/file_iframe_sandbox_document_write.html b/dom/security/test/csp/file_iframe_sandbox_document_write.html new file mode 100644 index 000000000..cdfa87c8f --- /dev/null +++ b/dom/security/test/csp/file_iframe_sandbox_document_write.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> <meta charset="utf-8"> </head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.postMessage({ok: result, desc: desc}, "*"); + } + function doStuff() { + var beforePrincipal = SpecialPowers.wrap(document).nodePrincipal; + document.open(); + document.write("rewritten sandboxed document"); + document.close(); + var afterPrincipal = SpecialPowers.wrap(document).nodePrincipal; + ok(beforePrincipal.equals(afterPrincipal), + "document.write() does not change underlying principal"); + } +</script> +<body onLoad='doStuff();'> + sandboxed with allow-scripts +</body> +</html> diff --git a/dom/security/test/csp/file_iframe_sandbox_srcdoc.html b/dom/security/test/csp/file_iframe_sandbox_srcdoc.html new file mode 100644 index 000000000..bc700ed68 --- /dev/null +++ b/dom/security/test/csp/file_iframe_sandbox_srcdoc.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1073952 - CSP should restrict scripts in srcdoc iframe even if sandboxed</title> +</head> +<body> +<iframe srcdoc="<img src=x onerror='parent.postMessage({result: `unexpected-csp-violation`}, `*`);'>" + sandbox="allow-scripts"></iframe> +</body> +</html> diff --git a/dom/security/test/csp/file_iframe_sandbox_srcdoc.html^headers^ b/dom/security/test/csp/file_iframe_sandbox_srcdoc.html^headers^ new file mode 100644 index 000000000..cf869e07d --- /dev/null +++ b/dom/security/test/csp/file_iframe_sandbox_srcdoc.html^headers^ @@ -0,0 +1 @@ +content-security-policy: default-src *; diff --git a/dom/security/test/csp/file_iframe_srcdoc.sjs b/dom/security/test/csp/file_iframe_srcdoc.sjs new file mode 100644 index 000000000..6de8a029e --- /dev/null +++ b/dom/security/test/csp/file_iframe_srcdoc.sjs @@ -0,0 +1,79 @@ +// Custom *.sjs file specifically for the needs of +// https://bugzilla.mozilla.org/show_bug.cgi?id=1073952 + +"use strict"; +Components.utils.importGlobalProperties(["URLSearchParams"]); + +const SCRIPT = ` + <script> + parent.parent.postMessage({result: "allowed"}, "*"); + </script>`; + +const SIMPLE_IFRAME_SRCDOC = ` + <!DOCTYPE html> + <html> + <head><meta charset="utf-8"></head> + <body> + <iframe sandbox="allow-scripts" srcdoc="` + SCRIPT + `"></iframe> + </body> + </html>`; + +const INNER_SRCDOC_IFRAME = ` + <iframe sandbox='allow-scripts' srcdoc='<script> + parent.parent.parent.postMessage({result: "allowed"}, "*"); + </script>'> + </iframe>`; + +const NESTED_IFRAME_SRCDOC = ` + <!DOCTYPE html> + <html> + <head><meta charset="utf-8"></head> + <body> + <iframe sandbox="allow-scripts" srcdoc="` + INNER_SRCDOC_IFRAME + `"></iframe> + </body> + </html>`; + + +const INNER_DATAURI_IFRAME = ` + <iframe sandbox='allow-scripts' src='data:text/html,<script> + parent.parent.parent.postMessage({result: "allowed"}, "*"); + </script>'> + </iframe>`; + +const NESTED_IFRAME_SRCDOC_DATAURI = ` + <!DOCTYPE html> + <html> + <head><meta charset="utf-8"></head> + <body> + <iframe sandbox="allow-scripts" srcdoc="` + INNER_DATAURI_IFRAME + `"></iframe> + </body> + </html>`; + +function handleRequest(request, response) { + const query = new URLSearchParams(request.queryString); + + response.setHeader("Cache-Control", "no-cache", false); + if (typeof query.get("csp") === "string") { + response.setHeader("Content-Security-Policy", query.get("csp"), false); + } + response.setHeader("Content-Type", "text/html", false); + + if (query.get("action") === "simple_iframe_srcdoc") { + response.write(SIMPLE_IFRAME_SRCDOC); + return; + } + + if (query.get("action") === "nested_iframe_srcdoc") { + response.write(NESTED_IFRAME_SRCDOC); + return; + } + + if (query.get("action") === "nested_iframe_srcdoc_datauri") { + response.write(NESTED_IFRAME_SRCDOC_DATAURI); + return; + } + + // we should never get here, but just in case + // return something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/csp/file_ignore_unsafe_inline.html b/dom/security/test/csp/file_ignore_unsafe_inline.html new file mode 100644 index 000000000..b0d44b570 --- /dev/null +++ b/dom/security/test/csp/file_ignore_unsafe_inline.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1004703 - ignore 'unsafe-inline' if nonce- or hash-source specified</title> +</head> +<body> +<div id="testdiv">a</div> + +<!-- first script whitelisted by 'unsafe-inline' --> +<script type="application/javascript"> +document.getElementById('testdiv').innerHTML += 'b'; +</script> + +<!-- second script whitelisted by hash --> +<!-- sha256-uJXAPKP5NZxnVMZMUkDofh6a9P3UMRc1CRTevVPS/rI= --> +<script type="application/javascript"> +document.getElementById('testdiv').innerHTML += 'c'; +</script> + +<!-- thrid script whitelisted by nonce --> +<script type="application/javascript" nonce="FooNonce"> +document.getElementById('testdiv').innerHTML += 'd'; +</script> + +</body> +</html> diff --git a/dom/security/test/csp/file_ignore_unsafe_inline_multiple_policies_server.sjs b/dom/security/test/csp/file_ignore_unsafe_inline_multiple_policies_server.sjs new file mode 100644 index 000000000..0f0a8ad3e --- /dev/null +++ b/dom/security/test/csp/file_ignore_unsafe_inline_multiple_policies_server.sjs @@ -0,0 +1,56 @@ +// custom *.sjs file specifically for the needs of: +// * Bug 1004703 - ignore 'unsafe-inline' if nonce- or hash-source specified +// * Bug 1198422: should not block inline script if default-src is not specified + +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +function loadHTMLFromFile(path) { + // Load the HTML to return in the response from file. + // Since it's relative to the cwd of the test runner, we start there and + // append to get to the actual path of the file. + var testHTMLFile = + Components.classes["@mozilla.org/file/directory_service;1"]. + getService(Components.interfaces.nsIProperties). + get("CurWorkD", Components.interfaces.nsILocalFile); + var dirs = path.split("/"); + for (var i = 0; i < dirs.length; i++) { + testHTMLFile.append(dirs[i]); + } + var testHTMLFileStream = + Components.classes["@mozilla.org/network/file-input-stream;1"]. + createInstance(Components.interfaces.nsIFileInputStream); + testHTMLFileStream.init(testHTMLFile, -1, 0, 0); + var testHTML = NetUtil.readInputStreamToString(testHTMLFileStream, testHTMLFileStream.available()); + return testHTML; +} + + +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + var csp1 = (query['csp1']) ? unescape(query['csp1']) : ""; + var csp2 = (query['csp2']) ? unescape(query['csp2']) : ""; + var file = unescape(query['file']); + + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // deliver the CSP encoded in the URI + // please note that comma separation of two policies + // acts like sending *two* separate policies + var csp = csp1; + if (csp2 !== "") { + csp += ", " + csp2; + } + response.setHeader("Content-Security-Policy", csp, false); + + // Send HTML to test allowed/blocked behaviors + response.setHeader("Content-Type", "text/html", false); + + response.write(loadHTMLFromFile(file)); +} diff --git a/dom/security/test/csp/file_inlinescript.html b/dom/security/test/csp/file_inlinescript.html new file mode 100644 index 000000000..55a9b9b18 --- /dev/null +++ b/dom/security/test/csp/file_inlinescript.html @@ -0,0 +1,15 @@ +<html>
+<head>
+ <title>CSP inline script tests</title>
+</head>
+<body onload="window.parent.postMessage('body-onload-fired', '*')">
+ <script type="text/javascript">
+ window.parent.postMessage("text-node-fired", "*");
+ </script>
+
+ <iframe src='javascript:window.parent.parent.postMessage("javascript-uri-fired", "*")'></iframe>
+
+ <a id='anchortoclick' href='javascript:window.parent.postMessage("javascript-uri-anchor-fired", "*")'>testlink</a>
+
+</body>
+</html>
diff --git a/dom/security/test/csp/file_inlinestyle_main.html b/dom/security/test/csp/file_inlinestyle_main.html new file mode 100644 index 000000000..a0d296988 --- /dev/null +++ b/dom/security/test/csp/file_inlinestyle_main.html @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<html> + <head> + <title>CSP inline script tests</title> + <!-- content= "div#linkstylediv { color: #0f0; }" --> + <link rel="stylesheet" type="text/css" + href='file_CSP.sjs?type=text/css&content=div%23linkstylediv%20%7B%20color%3A%20%230f0%3B%20%7D' /> + <!-- content= "div#modifycsstextdiv { color: #0f0; }" --> + <link rel="stylesheet" type="text/css" + href='file_CSP.sjs?type=text/css&content=div%23modifycsstextdiv%20%7B%20color%3A%20%23f00%3B%20%7D' /> + <script> + function cssTest() { + var elem = document.getElementById('csstextstylediv'); + elem.style.cssText = "color: #00FF00;"; + getComputedStyle(elem, null).color; + + document.styleSheets[1].cssRules[0].style.cssText = "color: #00FF00;"; + elem = document.getElementById('modifycsstextdiv'); + getComputedStyle(elem, null).color; + } + </script> + </head> + <body onload='cssTest()'> + + <style type="text/css"> + div#inlinestylediv { + color: #FF0000; + } + </style> + + <div id='linkstylediv'>Link tag (external) stylesheet test (should be green)</div> + <div id='inlinestylediv'>Inline stylesheet test (should be black)</div> + <div id='attrstylediv' style="color: #FF0000;">Attribute stylesheet test (should be black)</div> + <div id='csstextstylediv'>cssText test (should be black)</div> + <div id='modifycsstextdiv'> modify rule from style sheet via cssText(should be green) </div> + + <!-- tests for SMIL stuff - animations --> + <svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + width="100%" + height="100px"> + + <!-- Animates XML attribute, which is mapped into style. --> + <text id="xmlTest" x="0" y="15"> + This shouldn't be red since the animation should be blocked by CSP. + + <animate attributeName="fill" attributeType="XML" + values="red;orange;red" dur="2s" + repeatCount="indefinite" /> + </text> + + <!-- Animates override value for CSS property. --> + <text id="cssOverrideTest" x="0" y="35"> + This shouldn't be red since the animation should be blocked by CSP. + + <animate attributeName="fill" attributeType="CSS" + values="red;orange;red" dur="2s" + repeatCount="indefinite" /> + </text> + + <!-- Animates override value for CSS property targeted via ID. --> + <text id="cssOverrideTestById" x="0" y="55"> + This shouldn't be red since the animation should be blocked by CSP. + </text> + <animate xlink:href="#cssOverrideTestById" + attributeName="fill" + values="red;orange;red" + dur="2s" repeatCount="indefinite" /> + + <!-- Sets value for CSS property targeted via ID. --> + <text id="cssSetTestById" x="0" y="75"> + This shouldn't be red since the <set> should be blocked by CSP. + </text> + <set xlink:href="#cssSetTestById" + attributeName="fill" + to="red" /> + </svg> + </body> +</html> diff --git a/dom/security/test/csp/file_inlinestyle_main.html^headers^ b/dom/security/test/csp/file_inlinestyle_main.html^headers^ new file mode 100644 index 000000000..7b6a25167 --- /dev/null +++ b/dom/security/test/csp/file_inlinestyle_main.html^headers^ @@ -0,0 +1,2 @@ +Content-Security-Policy: default-src 'self' ; script-src 'self' 'unsafe-inline' +Cache-Control: no-cache diff --git a/dom/security/test/csp/file_inlinestyle_main_allowed.html b/dom/security/test/csp/file_inlinestyle_main_allowed.html new file mode 100644 index 000000000..9b533ef07 --- /dev/null +++ b/dom/security/test/csp/file_inlinestyle_main_allowed.html @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<html> + <head> + <title>CSP inline script tests</title> + <!-- content= "div#linkstylediv { color: #0f0; }" --> + <link rel="stylesheet" type="text/css" + href='file_CSP.sjs?type=text/css&content=div%23linkstylediv%20%7B%20color%3A%20%230f0%3B%20%7D' /> + <!-- content= "div#modifycsstextdiv { color: #f00; }" --> + <link rel="stylesheet" type="text/css" + href='file_CSP.sjs?type=text/css&content=div%23modifycsstextdiv%20%7B%20color%3A%20%23f00%3B%20%7D' /> + <script> + function cssTest() { + // CSSStyleDeclaration.cssText + var elem = document.getElementById('csstextstylediv'); + elem.style.cssText = "color: #00FF00;"; + + // If I call getComputedStyle as below, this test passes as the parent page + // correctly detects that the text is colored green - if I remove this, getComputedStyle + // thinks the text is black when called by the parent page. + getComputedStyle(elem, null).color; + + document.styleSheets[1].cssRules[0].style.cssText = "color: #00FF00;"; + elem = document.getElementById('modifycsstextdiv'); + getComputedStyle(elem, null).color; + } + </script> + </head> + <body onload='cssTest()'> + + <style type="text/css"> + div#inlinestylediv { + color: #00FF00; + } + </style> + + <div id='linkstylediv'>Link tag (external) stylesheet test (should be green)</div> + <div id='inlinestylediv'>Inline stylesheet test (should be green)</div> + <div id='attrstylediv' style="color: #00FF00;">Attribute stylesheet test (should be green)</div> + <div id='csstextstylediv'>style.cssText test (should be green)</div> + <div id='modifycsstextdiv'> modify rule from style sheet via cssText(should be green) </div> + + <!-- tests for SMIL stuff - animations --> + <svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + width="100%" + height="100px"> + + <!-- Animates XML attribute, which is mapped into style. --> + <text id="xmlTest" x="0" y="15"> + This should be green since the animation should be allowed by CSP. + + <animate attributeName="fill" attributeType="XML" + values="lime;green;lime" dur="2s" + repeatCount="indefinite" /> + </text> + + <!-- Animates override value for CSS property. --> + <text id="cssOverrideTest" x="0" y="35"> + This should be green since the animation should be allowed by CSP. + + <animate attributeName="fill" attributeType="CSS" + values="lime;green;lime" dur="2s" + repeatCount="indefinite" /> + </text> + + <!-- Animates override value for CSS property targeted via ID. --> + <text id="cssOverrideTestById" x="0" y="55"> + This should be green since the animation should be allowed by CSP. + </text> + <animate xlink:href="#cssOverrideTestById" + attributeName="fill" + values="lime;green;lime" + dur="2s" repeatCount="indefinite" /> + + <!-- Sets value for CSS property targeted via ID. --> + <text id="cssSetTestById" x="0" y="75"> + This should be green since the <set> should be allowed by CSP. + </text> + <set xlink:href="#cssSetTestById" + attributeName="fill" + to="lime" /> + </svg> + </body> +</html> diff --git a/dom/security/test/csp/file_inlinestyle_main_allowed.html^headers^ b/dom/security/test/csp/file_inlinestyle_main_allowed.html^headers^ new file mode 100644 index 000000000..621d2536b --- /dev/null +++ b/dom/security/test/csp/file_inlinestyle_main_allowed.html^headers^ @@ -0,0 +1,2 @@ +Content-Security-Policy: default-src 'self' ; script-src 'self' 'unsafe-inline' ; style-src 'self' 'unsafe-inline' +Cache-Control: no-cache diff --git a/dom/security/test/csp/file_invalid_source_expression.html b/dom/security/test/csp/file_invalid_source_expression.html new file mode 100644 index 000000000..83bb0ec0c --- /dev/null +++ b/dom/security/test/csp/file_invalid_source_expression.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1086612 - CSP: Let source expression be the empty set in case no valid source can be parsed</title> + </head> + <body> + <div id="testdiv">blocked</div> + <!-- Note, we reuse file_path_matching.js which only updates the testdiv to 'allowed' if loaded !--> + <script src="http://test1.example.com/tests/dom/security/test/csp/file_path_matching.js"></script> +</body> +</html> diff --git a/dom/security/test/csp/file_leading_wildcard.html b/dom/security/test/csp/file_leading_wildcard.html new file mode 100644 index 000000000..ea5e99344 --- /dev/null +++ b/dom/security/test/csp/file_leading_wildcard.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1032303 - CSP - Keep FULL STOP when matching *.foo.com to disallow loads from foo.com</title> + </head> + <body> + <!-- Please note that both scripts do *not* exist in the file system --> + <script src="http://test1.example.com/tests/dom/security/test/csp/leading_wildcard_allowed.js" ></script> + <script src="http://example.com/tests/dom/security/test/csp/leading_wildcard_blocked.js" ></script> +</body> +</html> diff --git a/dom/security/test/csp/file_main.html b/dom/security/test/csp/file_main.html new file mode 100644 index 000000000..ddc838261 --- /dev/null +++ b/dom/security/test/csp/file_main.html @@ -0,0 +1,55 @@ +<html> + <head> + <link rel='stylesheet' type='text/css' + href='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=style_bad&type=text/css' /> + <link rel='stylesheet' type='text/css' + href='file_CSP.sjs?testid=style_good&type=text/css' /> + + + <style> + /* CSS font embedding tests */ + @font-face { + font-family: "arbitrary_good"; + src: url('file_CSP.sjs?testid=font_good&type=application/octet-stream'); + } + @font-face { + font-family: "arbitrary_bad"; + src: url('http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=font_bad&type=application/octet-stream'); + } + + .div_arbitrary_good { font-family: "arbitrary_good"; } + .div_arbitrary_bad { font-family: "arbitrary_bad"; } + </style> + </head> + <body> + <!-- these should be stopped by CSP. :) --> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img_bad&type=img/png"> </img> + <audio src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=media_bad&type=audio/vorbis"></audio> + <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script_bad&type=text/javascript'></script> + <iframe src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=frame_bad&content=FAIL'></iframe> + <object width="10" height="10"> + <param name="movie" value="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=object_bad&type=application/x-shockwave-flash"> + <embed src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=object_bad&type=application/x-shockwave-flash"></embed> + </object> + + <!-- these should load ok. :) --> + <img src="file_CSP.sjs?testid=img_good&type=img/png" /> + <audio src="file_CSP.sjs?testid=media_good&type=audio/vorbis"></audio> + <script src='file_CSP.sjs?testid=script_good&type=text/javascript'></script> + <iframe src='file_CSP.sjs?testid=frame_good&content=PASS'></iframe> + + <object width="10" height="10"> + <param name="movie" value="file_CSP.sjs?testid=object_good&type=application/x-shockwave-flash"> + <embed src="file_CSP.sjs?testid=object_good&type=application/x-shockwave-flash"></embed> + </object> + + <!-- XHR tests... they're taken care of in this script, + and since the URI doesn't have any 'testid' values, + it will just be ignored by the test framework. --> + <script src='file_main.js'></script> + + <!-- Support elements for the @font-face test --> + <div class="div_arbitrary_good">arbitrary good</div> + <div class="div_arbitrary_bad">arbitrary_bad</div> + </body> +</html> diff --git a/dom/security/test/csp/file_main.html^headers^ b/dom/security/test/csp/file_main.html^headers^ new file mode 100644 index 000000000..3338de389 --- /dev/null +++ b/dom/security/test/csp/file_main.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' blob: ; style-src 'unsafe-inline' 'self' diff --git a/dom/security/test/csp/file_main.js b/dom/security/test/csp/file_main.js new file mode 100644 index 000000000..d5afe580f --- /dev/null +++ b/dom/security/test/csp/file_main.js @@ -0,0 +1,51 @@ +function doXHR(uri, callback) { + try { + var xhr = new XMLHttpRequest(); + xhr.open("GET", uri); + xhr.responseType = "blob"; + xhr.send(); + xhr.onload = function () { + if (callback) callback(xhr.response); + } + } catch(ex) {} +} + +doXHR("http://mochi.test:8888/tests/dom/security/test/csp/file_CSP.sjs?testid=xhr_good"); +doXHR("http://example.com/tests/dom/security/test/csp/file_CSP.sjs?testid=xhr_bad"); +fetch("http://mochi.test:8888/tests/dom/security/test/csp/file_CSP.sjs?testid=fetch_good"); +fetch("http://example.com/tests/dom/security/test/csp/file_CSP.sjs?testid=fetch_bad"); +navigator.sendBeacon("http://mochi.test:8888/tests/dom/security/test/csp/file_CSP.sjs?testid=beacon_good"); +navigator.sendBeacon("http://example.com/tests/dom/security/test/csp/file_CSP.sjs?testid=beacon_bad"); + +var topWorkerBlob; +var nestedWorkerBlob; + +doXHR("file_main_worker.js", function (topResponse) { + topWorkerBlob = URL.createObjectURL(topResponse); + doXHR("file_child_worker.js", function (response) { + nestedWorkerBlob = URL.createObjectURL(response); + runWorker(); + }); +}); + +function runWorker() { + // Top level worker, no subworker + // Worker does not inherit CSP from owner document + new Worker("file_main_worker.js").postMessage({inherited : "none"}); + + // Top level worker, no subworker + // Worker inherits CSP from owner document + new Worker(topWorkerBlob).postMessage({inherited : "document"}); + + // Subworker + // Worker does not inherit CSP from parent worker + new Worker("file_main_worker.js").postMessage({inherited : "none", nested : nestedWorkerBlob}); + + // Subworker + // Worker inherits CSP from parent worker + new Worker("file_main_worker.js").postMessage({inherited : "parent", nested : nestedWorkerBlob}); + + // Subworker + // Worker inherits CSP from owner document -> parent worker -> subworker + new Worker(topWorkerBlob).postMessage({inherited : "document", nested : nestedWorkerBlob}); +} diff --git a/dom/security/test/csp/file_main_worker.js b/dom/security/test/csp/file_main_worker.js new file mode 100644 index 000000000..d1314c843 --- /dev/null +++ b/dom/security/test/csp/file_main_worker.js @@ -0,0 +1,48 @@ +function doXHR(uri) { + try { + var xhr = new XMLHttpRequest(); + xhr.open("GET", uri); + xhr.send(); + } catch(ex) {} +} + +var sameBase = "http://mochi.test:8888/tests/dom/security/test/csp/file_CSP.sjs?testid="; +var crossBase = "http://example.com/tests/dom/security/test/csp/file_CSP.sjs?testid="; + +onmessage = (e) => { + // Tests of nested worker + if (e.data.nested) { + if (e.data.inherited != "none") { + // Worker inherits CSP + new Worker(e.data.nested).postMessage({inherited : e.data.inherited}); + } + else { + // Worker does not inherit CSP + new Worker("file_child_worker.js").postMessage({inherited : e.data.inherited}); + } + return; + } + + //Tests of top level worker + for (base of [sameBase, crossBase]) { + var prefix; + var suffix; + if (e.data.inherited != "none") { + // Top worker inherits CSP from owner document + prefix = base + "worker_inherited_"; + suffix = base == sameBase ? "_good" : "_bad"; + } + else { + // Top worker delivers CSP from HTTP header + prefix = base + "worker_"; + suffix = base == sameBase ? "_same_bad" : "_cross_good"; + } + + doXHR(prefix + "xhr" + suffix); + fetch(prefix + "fetch" + suffix); + try { + if (e.data.inherited == "none") suffix = base == sameBase ? "_same_good" : "_cross_bad"; + importScripts(prefix + "script" + suffix); + } catch(ex) {} + } +} diff --git a/dom/security/test/csp/file_main_worker.js^headers^ b/dom/security/test/csp/file_main_worker.js^headers^ new file mode 100644 index 000000000..3f5008ca6 --- /dev/null +++ b/dom/security/test/csp/file_main_worker.js^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' blob: ; connect-src http://example.com diff --git a/dom/security/test/csp/file_meta_element.html b/dom/security/test/csp/file_meta_element.html new file mode 100644 index 000000000..1d8ec6aa9 --- /dev/null +++ b/dom/security/test/csp/file_meta_element.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" + content= "img-src 'none'; script-src 'unsafe-inline'; report-uri http://www.example.com; frame-ancestors https:; sandbox allow-scripts"> + <title>Bug 663570 - Implement Content Security Policy via meta tag</title> +</head> +<body> + + <!-- try to load an image which is forbidden by meta CSP --> + <img id="testimage" src="http://mochi.test:8888/tests/image/test/mochitest/blue.png"></img> + + <script type="application/javascript"> + var myImg = document.getElementById("testimage"); + myImg.onload = function(e) { + window.parent.postMessage({result: "img-loaded"}, "*"); + }; + myImg.onerror = function(e) { + window.parent.postMessage({result: "img-blocked"}, "*"); + }; + </script> + +</body> +</html> diff --git a/dom/security/test/csp/file_meta_header_dual.sjs b/dom/security/test/csp/file_meta_header_dual.sjs new file mode 100644 index 000000000..ddc38ffe5 --- /dev/null +++ b/dom/security/test/csp/file_meta_header_dual.sjs @@ -0,0 +1,98 @@ +// Custom *.sjs file specifically for the needs of Bug: +// Bug 663570 - Implement Content Security Policy via meta tag + +const HTML_HEAD = + "<!DOCTYPE HTML>" + + "<html>" + + "<head>" + + "<meta charset='utf-8'>" + + "<title>Bug 663570 - Implement Content Security Policy via <meta> tag</title>"; + +const HTML_BODY = + "</head>" + + "<body>" + + "<img id='testimage' src='http://mochi.test:8888/tests/image/test/mochitest/blue.png'></img>" + + "<script type='application/javascript'>" + + " var myImg = document.getElementById('testimage');" + + " myImg.onload = function(e) {" + + " window.parent.postMessage({result: 'img-loaded'}, '*');" + + " };" + + " myImg.onerror = function(e) { " + + " window.parent.postMessage({result: 'img-blocked'}, '*');" + + " };" + + "</script>" + + "</body>" + + "</html>"; + +const META_CSP_BLOCK_IMG = + "<meta http-equiv=\"Content-Security-Policy\" content=\"img-src 'none'\">"; + +const META_CSP_ALLOW_IMG = + "<meta http-equiv=\"Content-Security-Policy\" content=\"img-src http://mochi.test:8888;\">"; + +const HEADER_CSP_BLOCK_IMG = "img-src 'none';"; + +const HEADER_CSP_ALLOW_IMG = "img-src http://mochi.test:8888"; + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + var queryString = request.queryString; + + if (queryString === "test1") { + /* load image without any CSP */ + response.write(HTML_HEAD + HTML_BODY); + return; + } + + if (queryString === "test2") { + /* load image where meta denies load */ + response.write(HTML_HEAD + META_CSP_BLOCK_IMG + HTML_BODY); + return; + } + + if (queryString === "test3") { + /* load image where meta allows load */ + response.write(HTML_HEAD + META_CSP_ALLOW_IMG + HTML_BODY); + return; + } + + if (queryString === "test4") { + /* load image where meta allows but header blocks */ + response.setHeader("Content-Security-Policy", HEADER_CSP_BLOCK_IMG, false); + response.write(HTML_HEAD + META_CSP_ALLOW_IMG + HTML_BODY); + return; + } + + if (queryString === "test5") { + /* load image where meta blocks but header allows */ + response.setHeader("Content-Security-Policy", HEADER_CSP_ALLOW_IMG, false); + response.write(HTML_HEAD + META_CSP_BLOCK_IMG + HTML_BODY); + return; + } + + if (queryString === "test6") { + /* load image where meta allows and header allows */ + response.setHeader("Content-Security-Policy", HEADER_CSP_ALLOW_IMG, false); + response.write(HTML_HEAD + META_CSP_ALLOW_IMG + HTML_BODY); + return; + } + + if (queryString === "test7") { + /* load image where meta1 allows but meta2 blocks */ + response.write(HTML_HEAD + META_CSP_ALLOW_IMG + META_CSP_BLOCK_IMG + HTML_BODY); + return; + } + + if (queryString === "test8") { + /* load image where meta1 allows and meta2 allows */ + response.write(HTML_HEAD + META_CSP_ALLOW_IMG + META_CSP_ALLOW_IMG + HTML_BODY); + return; + } + + // we should never get here, but just in case, return + // something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/csp/file_meta_whitespace_skipping.html b/dom/security/test/csp/file_meta_whitespace_skipping.html new file mode 100644 index 000000000..c0cfc8cc2 --- /dev/null +++ b/dom/security/test/csp/file_meta_whitespace_skipping.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <!-- Test all the different space characters within the meta csp: + * U+0020 space |   + * U+0009 tab | 	 + * U+000A line feed | 
 + * U+000C form feed |  + * U+000D carriage return | 
 + !--> + <meta http-equiv="Content-Security-Policy" + content= " + img-src  'none';   + script-src 'unsafe-inline' 
 + ; + style-src		 https://example.com
 + https://foo.com;;;;;; child-src foo.com + bar.com + 
;
 + font-src 'none'"> + <title>Bug 1261634 - Update whitespace skipping for meta csp</title> +</head> +<body> + <script type="application/javascript"> + // notify the including document that we are done parsing the meta csp + window.parent.postMessage({result: "meta-csp-parsed"}, "*"); + </script> + +</body> +</html> diff --git a/dom/security/test/csp/file_multi_policy_injection_bypass.html b/dom/security/test/csp/file_multi_policy_injection_bypass.html new file mode 100644 index 000000000..a3cb415a9 --- /dev/null +++ b/dom/security/test/csp/file_multi_policy_injection_bypass.html @@ -0,0 +1,15 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=717511 +--> + <body> + <!-- these should be stopped by CSP after fixing bug 717511. :) --> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img_bad&type=img/png"> </img> + <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script_bad&type=text/javascript'></script> + + <!-- these should load ok after fixing bug 717511. :) --> + <img src="file_CSP.sjs?testid=img_good&type=img/png" /> + <script src='file_CSP.sjs?testid=script_good&type=text/javascript'></script> + + </body> +</html> diff --git a/dom/security/test/csp/file_multi_policy_injection_bypass.html^headers^ b/dom/security/test/csp/file_multi_policy_injection_bypass.html^headers^ new file mode 100644 index 000000000..e1b64a922 --- /dev/null +++ b/dom/security/test/csp/file_multi_policy_injection_bypass.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self', default-src * diff --git a/dom/security/test/csp/file_multi_policy_injection_bypass_2.html b/dom/security/test/csp/file_multi_policy_injection_bypass_2.html new file mode 100644 index 000000000..3fa6c7ab9 --- /dev/null +++ b/dom/security/test/csp/file_multi_policy_injection_bypass_2.html @@ -0,0 +1,15 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=717511 +--> + <body> + <!-- these should be stopped by CSP after fixing bug 717511. :) --> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img2_bad&type=img/png"> </img> + <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script2_bad&type=text/javascript'></script> + + <!-- these should load ok after fixing bug 717511. :) --> + <img src="file_CSP.sjs?testid=img2_good&type=img/png" /> + <script src='file_CSP.sjs?testid=script2_good&type=text/javascript'></script> + + </body> +</html> diff --git a/dom/security/test/csp/file_multi_policy_injection_bypass_2.html^headers^ b/dom/security/test/csp/file_multi_policy_injection_bypass_2.html^headers^ new file mode 100644 index 000000000..b523073cd --- /dev/null +++ b/dom/security/test/csp/file_multi_policy_injection_bypass_2.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' , default-src * diff --git a/dom/security/test/csp/file_multipart_testserver.sjs b/dom/security/test/csp/file_multipart_testserver.sjs new file mode 100644 index 000000000..d2eb58c82 --- /dev/null +++ b/dom/security/test/csp/file_multipart_testserver.sjs @@ -0,0 +1,50 @@ +// SJS file specifically for the needs of bug +// Bug 1223743 - CSP: Check baseChannel for CSP when loading multipart channel + +var CSP = "script-src 'unsafe-inline', img-src 'none'"; +var BOUNDARY = "fooboundary" + +// small red image +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="); + +var RESPONSE = ` + <script> + var myImg = new Image; + myImg.src = "file_multipart_testserver.sjs?img"; + myImg.onerror = function(e) { + window.parent.postMessage("img-blocked", "*"); + }; + myImg.onload = function() { + window.parent.postMessage("img-loaded", "*"); + }; + document.body.appendChild(myImg); + </script> +`; + +var myTimer; + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if (request.queryString == "doc") { + response.setHeader("Content-Security-Policy", CSP, false); + response.setHeader("Content-Type", "multipart/x-mixed-replace; boundary=" + BOUNDARY, false); + response.write(BOUNDARY + "\r\n"); + response.write(RESPONSE); + response.write(BOUNDARY + "\r\n"); + return; + } + + if (request.queryString == "img") { + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + return; + } + + // we should never get here - return something unexpected + response.write("d'oh"); +} diff --git a/dom/security/test/csp/file_nonce_source.html b/dom/security/test/csp/file_nonce_source.html new file mode 100644 index 000000000..bf14ffe64 --- /dev/null +++ b/dom/security/test/csp/file_nonce_source.html @@ -0,0 +1,73 @@ +<!doctype html> +<html> + <head> + <!-- external styles --> + <link rel='stylesheet' nonce="correctstylenonce" href="file_CSP.sjs?testid=external_style_correct_nonce_good&type=text/css" /> + <link rel='stylesheet' nonce="incorrectstylenonce" href="file_CSP.sjs?testid=external_style_incorrect_nonce_bad&type=text/css" /> + <link rel='stylesheet' nonce="correctscriptnonce" href="file_CSP.sjs?testid=external_style_correct_script_nonce_bad&type=text/css" /> + <link rel='stylesheet' href="file_CSP.sjs?testid=external_style_no_nonce_bad&type=text/css" /> + </head> + <body> + <!-- inline scripts --> + <ol> + <li id="inline-script-correct-nonce">(inline script with correct nonce) This text should be green.</li> + <li id="inline-script-incorrect-nonce">(inline script with incorrect nonce) This text should be black.</li> + <li id="inline-script-correct-style-nonce">(inline script with correct nonce for styles, but not for scripts) This text should be black.</li> + <li id="inline-script-no-nonce">(inline script with no nonce) This text should be black.</li> + </ol> + <script nonce="correctscriptnonce"> + document.getElementById("inline-script-correct-nonce").style.color = "rgb(0, 128, 0)"; + </script> + <script nonce="incorrectscriptnonce"> + document.getElementById("inline-script-incorrect-nonce").style.color = "rgb(255, 0, 0)"; + </script> + <script nonce="correctstylenonce"> + document.getElementById("inline-script-correct-style-nonce").style.color = "rgb(255, 0, 0)"; + </script> + <script> + document.getElementById("inline-script-no-nonce").style.color = "rgb(255, 0, 0)"; + </script> + + <!-- external scripts --> + <script nonce="correctscriptnonce" src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=external_script_correct_nonce_good&type=text/javascript"></script> + <script nonce="anothercorrectscriptnonce" src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=external_script_another_correct_nonce_good&type=text/javascript"></script> + <script nonce="incorrectscriptnonce" src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=external_script_incorrect_nonce_bad&type=text/javascript"></script> + <script nonce="correctstylenonce" src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=external_script_correct_style_nonce_bad&type=text/javascript"></script> + <script src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=external_script_no_nonce_bad&type=text/javascript"></script> + + <!-- This external script has the correct nonce and comes from a whitelisted URI. It should be allowed. --> + <script nonce="correctscriptnonce" src="file_CSP.sjs?testid=external_script_correct_nonce_correct_uri_good&type=text/javascript"></script> + <!-- This external script has an incorrect nonce, but comes from a whitelisted URI. It should be allowed. --> + <script nonce="incorrectscriptnonce" src="file_CSP.sjs?testid=external_script_incorrect_nonce_correct_uri_good&type=text/javascript"></script> + <!-- This external script has no nonce and comes from a whitelisted URI. It should be allowed. --> + <script src="file_CSP.sjs?testid=external_script_no_nonce_correct_uri_good&type=text/javascript"></script> + + <!-- inline styles --> + <ol> + <li id=inline-style-correct-nonce> + (inline style with correct nonce) This text should be green + </li> + <li id=inline-style-incorrect-nonce> + (inline style with incorrect nonce) This text should be black + </li> + <li id=inline-style-correct-script-nonce> + (inline style with correct script, not style, nonce) This text should be black + </li> + <li id=inline-style-no-nonce> + (inline style with no nonce) This text should be black + </li> + </ol> + <style nonce=correctstylenonce> + li#inline-style-correct-nonce { color: green; } + </style> + <style nonce=incorrectstylenonce> + li#inline-style-incorrect-nonce { color: red; } + </style> + <style nonce=correctscriptnonce> + li#inline-style-correct-script-nonce { color: red; } + </style> + <style> + li#inline-style-no-nonce { color: red; } + </style> + </body> +</html> diff --git a/dom/security/test/csp/file_nonce_source.html^headers^ b/dom/security/test/csp/file_nonce_source.html^headers^ new file mode 100644 index 000000000..865e5fe98 --- /dev/null +++ b/dom/security/test/csp/file_nonce_source.html^headers^ @@ -0,0 +1,2 @@ +Content-Security-Policy: script-src 'self' 'nonce-correctscriptnonce' 'nonce-anothercorrectscriptnonce'; style-src 'nonce-correctstylenonce'; +Cache-Control: no-cache diff --git a/dom/security/test/csp/file_null_baseuri.html b/dom/security/test/csp/file_null_baseuri.html new file mode 100644 index 000000000..f995688b1 --- /dev/null +++ b/dom/security/test/csp/file_null_baseuri.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1121857 - document.baseURI should not get blocked if baseURI is null</title> + </head> + <body> + <script type="text/javascript"> + // check the initial base-uri + window.parent.postMessage({baseURI: document.baseURI, test: "initial_base_uri"}, "*"); + + // append a child and check the base-uri + var baseTag = document.head.appendChild(document.createElement('base')); + baseTag.href = 'http://www.base-tag.com'; + window.parent.postMessage({baseURI: document.baseURI, test: "changed_base_uri"}, "*"); + + // remove the child and check that the base-uri is back to the initial one + document.head.remove(baseTag); + window.parent.postMessage({baseURI: document.baseURI, test: "initial_base_uri"}, "*"); + </script> +</body> +</html> diff --git a/dom/security/test/csp/file_path_matching.html b/dom/security/test/csp/file_path_matching.html new file mode 100644 index 000000000..662fbfb8a --- /dev/null +++ b/dom/security/test/csp/file_path_matching.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 808292 - Implement path-level host-source matching to CSP</title> + </head> + <body> + <div id="testdiv">blocked</div> + <script src="http://test1.example.com/tests/dom/security/test/csp/file_path_matching.js#foo"></script> +</body> +</html> diff --git a/dom/security/test/csp/file_path_matching.js b/dom/security/test/csp/file_path_matching.js new file mode 100644 index 000000000..09286d42e --- /dev/null +++ b/dom/security/test/csp/file_path_matching.js @@ -0,0 +1 @@ +document.getElementById("testdiv").innerHTML = "allowed"; diff --git a/dom/security/test/csp/file_path_matching_incl_query.html b/dom/security/test/csp/file_path_matching_incl_query.html new file mode 100644 index 000000000..50af2b143 --- /dev/null +++ b/dom/security/test/csp/file_path_matching_incl_query.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1147026 - CSP should ignore query string when checking a resource load</title> + </head> + <body> + <div id="testdiv">blocked</div> + <script src="http://test1.example.com/tests/dom/security/test/csp/file_path_matching.js?val=foo"></script> +</body> +</html> diff --git a/dom/security/test/csp/file_path_matching_redirect.html b/dom/security/test/csp/file_path_matching_redirect.html new file mode 100644 index 000000000..a16cc90ec --- /dev/null +++ b/dom/security/test/csp/file_path_matching_redirect.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 808292 - Implement path-level host-source matching to CSP</title> + </head> + <body> + <div id="testdiv">blocked</div> + <script src="http://example.com/tests/dom/security/test/csp/file_path_matching_redirect_server.sjs"></script> +</body> +</html> diff --git a/dom/security/test/csp/file_path_matching_redirect_server.sjs b/dom/security/test/csp/file_path_matching_redirect_server.sjs new file mode 100644 index 000000000..49a3160f4 --- /dev/null +++ b/dom/security/test/csp/file_path_matching_redirect_server.sjs @@ -0,0 +1,13 @@ +// Redirect server specifically to handle redirects +// for path-level host-source matching +// see https://bugzilla.mozilla.org/show_bug.cgi?id=808292 + +function handleRequest(request, response) +{ + + var newLocation = "http://test1.example.com/tests/dom/security/test/csp/file_path_matching.js"; + + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Location", newLocation, false); +} diff --git a/dom/security/test/csp/file_ping.html b/dom/security/test/csp/file_ping.html new file mode 100644 index 000000000..8aaf34cc3 --- /dev/null +++ b/dom/security/test/csp/file_ping.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1100181 - CSP: Enforce connect-src when submitting pings</title> +</head> +<body> + <!-- we are using an image for the test, but can be anything --> + <a id="testlink" + href="http://mochi.test:8888/tests/image/test/mochitest/blue.png" + ping="http://mochi.test:8888/tests/image/test/mochitest/blue.png?send-ping"> + Send ping + </a> + + <script type="text/javascript"> + var link = document.getElementById("testlink"); + link.click(); + </script> +</body> +</html> diff --git a/dom/security/test/csp/file_policyuri_regression_from_multipolicy.html b/dom/security/test/csp/file_policyuri_regression_from_multipolicy.html new file mode 100644 index 000000000..2a75eef7e --- /dev/null +++ b/dom/security/test/csp/file_policyuri_regression_from_multipolicy.html @@ -0,0 +1,9 @@ +<!doctype html> +<html> + <body> + <div id=testdiv>Inline script didn't run</div> + <script> + document.getElementById('testdiv').innerHTML = "Inline Script Executed"; + </script> + </body> +</html> diff --git a/dom/security/test/csp/file_policyuri_regression_from_multipolicy.html^headers^ b/dom/security/test/csp/file_policyuri_regression_from_multipolicy.html^headers^ new file mode 100644 index 000000000..c4ff8ea9f --- /dev/null +++ b/dom/security/test/csp/file_policyuri_regression_from_multipolicy.html^headers^ @@ -0,0 +1 @@ +content-security-policy-report-only: policy-uri /tests/dom/security/test/csp/file_policyuri_regression_from_multipolicy_policy diff --git a/dom/security/test/csp/file_policyuri_regression_from_multipolicy_policy b/dom/security/test/csp/file_policyuri_regression_from_multipolicy_policy new file mode 100644 index 000000000..a5c610cd7 --- /dev/null +++ b/dom/security/test/csp/file_policyuri_regression_from_multipolicy_policy @@ -0,0 +1 @@ +default-src 'self'; diff --git a/dom/security/test/csp/file_redirect_content.sjs b/dom/security/test/csp/file_redirect_content.sjs new file mode 100644 index 000000000..080f179cd --- /dev/null +++ b/dom/security/test/csp/file_redirect_content.sjs @@ -0,0 +1,38 @@ +// https://bugzilla.mozilla.org/show_bug.cgi?id=650386 +// This SJS file serves file_redirect_content.html +// with a CSP that will trigger a violation and that will report it +// to file_redirect_report.sjs +// +// This handles 301, 302, 303 and 307 redirects. The HTTP status code +// returned/type of redirect to do comes from the query string +// parameter passed in from the test_bug650386_* files and then also +// uses that value in the report-uri parameter of the CSP +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + // this gets used in the CSP as part of the report URI. + var redirect = request.queryString; + + if (redirect < 301 || (redirect > 303 && redirect <= 306) || redirect > 307) { + // if we somehow got some bogus redirect code here, + // do a 302 redirect to the same URL as the report URI + // redirects to - this will fail the test. + var loc = "http://example.com/some/fake/path"; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", loc, false); + return; + } + + var csp = "default-src \'self\';report-uri http://mochi.test:8888/tests/dom/security/test/csp/file_redirect_report.sjs?" + redirect; + + response.setHeader("Content-Security-Policy", csp, false); + + // the actual file content. + // this image load will (intentionally) fail due to the CSP policy of default-src: 'self' + // specified by the CSP string above. + var content = "<!DOCTYPE HTML><html><body><img src = \"http://some.other.domain.example.com\"></body></html>"; + + response.write(content); + + return; +} diff --git a/dom/security/test/csp/file_redirect_report.sjs b/dom/security/test/csp/file_redirect_report.sjs new file mode 100644 index 000000000..9cc7e6548 --- /dev/null +++ b/dom/security/test/csp/file_redirect_report.sjs @@ -0,0 +1,17 @@ +// https://bugzilla.mozilla.org/show_bug.cgi?id=650386 +// This SJS file serves as CSP violation report target +// and issues a redirect, to make sure the browser does not post to the target +// of the redirect, per CSP spec. +// This handles 301, 302, 303 and 307 redirects. The HTTP status code +// returned/type of redirect to do comes from the query string +// parameter +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + var redirect = request.queryString; + + var loc = "http://example.com/some/fake/path"; + response.setStatusLine("1.1", redirect, "Found"); + response.setHeader("Location", loc, false); + return; +} diff --git a/dom/security/test/csp/file_redirect_worker.sjs b/dom/security/test/csp/file_redirect_worker.sjs new file mode 100644 index 000000000..27df6a5fd --- /dev/null +++ b/dom/security/test/csp/file_redirect_worker.sjs @@ -0,0 +1,34 @@ +// SJS file to serve resources for CSP redirect tests +// This file redirects to a specified resource. +const THIS_SITE = "http://mochi.test:8888"; +const OTHER_SITE = "http://example.com"; + +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + var resource = query['path']; + + response.setHeader("Cache-Control", "no-cache", false); + var loc = ''; + + // redirect to a resource on this site + if (query["redir"] == "same") { + loc = THIS_SITE+resource+"#"+query['page_id'] + } + + // redirect to a resource on a different site + else if (query["redir"] == "other") { + loc = OTHER_SITE+resource+"#"+query['page_id'] + } + + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", loc, false); + + response.write('<html><head><meta http-equiv="refresh" content="0; url='+loc+'">'); + return; +} diff --git a/dom/security/test/csp/file_redirects_main.html b/dom/security/test/csp/file_redirects_main.html new file mode 100644 index 000000000..d05af88fe --- /dev/null +++ b/dom/security/test/csp/file_redirects_main.html @@ -0,0 +1,37 @@ +<html> +<head> +<title>CSP redirect tests</title> +</head> +<body> +<div id="container"></div> +</body> + +<script> +var thisSite = "http://mochi.test:8888"; +var otherSite = "http://example.com"; +var page = "/tests/dom/security/test/csp/file_redirects_page.sjs"; + +var tests = { "font-src": thisSite+page+"?testid=font-src", + "frame-src": thisSite+page+"?testid=frame-src", + "img-src": thisSite+page+"?testid=img-src", + "media-src": thisSite+page+"?testid=media-src", + "object-src": thisSite+page+"?testid=object-src", + "script-src": thisSite+page+"?testid=script-src", + "style-src": thisSite+page+"?testid=style-src", + "xhr-src": thisSite+page+"?testid=xhr-src", + "from-worker": thisSite+page+"?testid=from-worker", + "from-blob-worker": thisSite+page+"?testid=from-blob-worker", + "img-src-from-css": thisSite+page+"?testid=img-src-from-css", + }; + +var container = document.getElementById("container"); + +// load each test in its own iframe +for (tid in tests) { + var i = document.createElement("iframe"); + i.id = tid; + i.src = tests[tid]; + container.appendChild(i); +} +</script> +</html> diff --git a/dom/security/test/csp/file_redirects_page.sjs b/dom/security/test/csp/file_redirects_page.sjs new file mode 100644 index 000000000..ced2d0787 --- /dev/null +++ b/dom/security/test/csp/file_redirects_page.sjs @@ -0,0 +1,103 @@ +// SJS file for CSP redirect mochitests +// This file serves pages which can optionally specify a Content Security Policy +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + var resource = "/tests/dom/security/test/csp/file_redirects_resource.sjs"; + + // CSP header value + response.setHeader("Content-Security-Policy", + "default-src 'self' blob: ; style-src 'self' 'unsafe-inline'", false); + + // downloadable font that redirects to another site + if (query["testid"] == "font-src") { + var resp = '<style type="text/css"> @font-face { font-family:' + + '"Redirecting Font"; src: url("' + resource + + '?res=font&redir=other&id=font-src-redir")} #test{font-family:' + + '"Redirecting Font"}</style></head><body>' + + '<div id="test">test</div></body>'; + response.write(resp); + return; + } + + // iframe that redirects to another site + if (query["testid"] == "frame-src") { + response.write('<iframe src="'+resource+'?res=iframe&redir=other&id=frame-src-redir"></iframe>'); + return; + } + + // image that redirects to another site + if (query["testid"] == "img-src") { + response.write('<img src="'+resource+'?res=image&redir=other&id=img-src-redir" />'); + return; + } + + // video content that redirects to another site + if (query["testid"] == "media-src") { + response.write('<video src="'+resource+'?res=media&redir=other&id=media-src-redir"></video>'); + return; + } + + // object content that redirects to another site + if (query["testid"] == "object-src") { + response.write('<object type="text/html" data="'+resource+'?res=object&redir=other&id=object-src-redir"></object>'); + return; + } + + // external script that redirects to another site + if (query["testid"] == "script-src") { + response.write('<script src="'+resource+'?res=script&redir=other&id=script-src-redir"></script>'); + return; + } + + // external stylesheet that redirects to another site + if (query["testid"] == "style-src") { + response.write('<link rel="stylesheet" type="text/css" href="'+resource+'?res=style&redir=other&id=style-src-redir"></link>'); + return; + } + + // script that XHR's to a resource that redirects to another site + if (query["testid"] == "xhr-src") { + response.write('<script src="'+resource+'?res=xhr"></script>'); + return; + } + + // for bug949706 + if (query["testid"] == "img-src-from-css") { + // loads a stylesheet, which in turn loads an image that redirects. + response.write('<link rel="stylesheet" type="text/css" href="'+resource+'?res=cssLoader&id=img-src-redir-from-css">'); + return; + } + + if (query["testid"] == "from-worker") { + // loads a script; launches a worker; that worker uses importscript; which then gets redirected + // So it's: + // <script src="res=loadWorkerThatMakesRequests"> + // .. loads Worker("res=makeRequestsWorker") + // .. calls importScript("res=script") + // .. calls xhr("res=xhr-resp") + // .. calls fetch("res=xhr-resp") + response.write('<script src="'+resource+'?res=loadWorkerThatMakesRequests&id=from-worker"></script>'); + return; + } + + if (query["testid"] == "from-blob-worker") { + // loads a script; launches a worker; that worker uses importscript; which then gets redirected + // So it's: + // <script src="res=loadBlobWorkerThatMakesRequests"> + // .. loads Worker("res=makeRequestsWorker") + // .. calls importScript("res=script") + // .. calls xhr("res=xhr-resp") + // .. calls fetch("res=xhr-resp") + response.write('<script src="'+resource+'?res=loadBlobWorkerThatMakesRequests&id=from-blob-worker"></script>'); + return; + } +} diff --git a/dom/security/test/csp/file_redirects_resource.sjs b/dom/security/test/csp/file_redirects_resource.sjs new file mode 100644 index 000000000..da138630f --- /dev/null +++ b/dom/security/test/csp/file_redirects_resource.sjs @@ -0,0 +1,149 @@ +// SJS file to serve resources for CSP redirect tests +// This file mimics serving resources, e.g. fonts, images, etc., which a CSP +// can include. The resource may redirect to a different resource, if specified. +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + var thisSite = "http://mochi.test:8888"; + var otherSite = "http://example.com"; + var resource = "/tests/dom/security/test/csp/file_redirects_resource.sjs"; + + response.setHeader("Cache-Control", "no-cache", false); + + // redirect to a resource on this site + if (query["redir"] == "same") { + var loc = thisSite+resource+"?res="+query["res"]+"&testid="+query["id"]; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", loc, false); + return; + } + + // redirect to a resource on a different site + else if (query["redir"] == "other") { + var loc = otherSite+resource+"?res="+query["res"]+"&testid="+query["id"]; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", loc, false); + return; + } + + // not a redirect. serve some content. + // the content doesn't have to be valid, since we're only checking whether + // the request for the content was sent or not. + + // downloadable font + if (query["res"] == "font") { + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Content-Type", "text/plain", false); + response.write("font data..."); + return; + } + + // iframe with arbitrary content + if (query["res"] == "iframe") { + response.setHeader("Content-Type", "text/html", false); + response.write("iframe content..."); + return; + } + + // image + if (query["res"] == "image") { + response.setHeader("Content-Type", "image/gif", false); + response.write("image data..."); + return; + } + + // media content, e.g. Ogg video + if (query["res"] == "media") { + response.setHeader("Content-Type", "video/ogg", false); + response.write("video data..."); + return; + } + + // plugin content, e.g. <object> + if (query["res"] == "object") { + response.setHeader("Content-Type", "text/html", false); + response.write("object data..."); + return; + } + + // script + if (query["res"] == "script") { + response.setHeader("Content-Type", "application/javascript", false); + response.write("some script..."); + return; + } + + // external stylesheet + if (query["res"] == "style") { + response.setHeader("Content-Type", "text/css", false); + response.write("css data..."); + return; + } + + // internal stylesheet that loads an image from an external site + if (query["res"] == "cssLoader") { + let bgURL = thisSite + resource + '?redir=other&res=image&id=' + query["id"]; + response.setHeader("Content-Type", "text/css", false); + response.write("body { background:url('" + bgURL + "'); }"); + return; + } + + // script that loads an internal worker that uses importScripts on a redirect + // to an external script. + if (query["res"] == "loadWorkerThatMakesRequests") { + // this creates a worker (same origin) that imports a redirecting script. + let workerURL = thisSite + resource + '?res=makeRequestsWorker&id=' + query["id"]; + response.setHeader("Content-Type", "application/javascript", false); + response.write("new Worker('" + workerURL + "');"); + return; + } + + // script that loads an internal worker that uses importScripts on a redirect + // to an external script. + if (query["res"] == "loadBlobWorkerThatMakesRequests") { + // this creates a worker (same origin) that imports a redirecting script. + let workerURL = thisSite + resource + '?res=makeRequestsWorker&id=' + query["id"]; + response.setHeader("Content-Type", "application/javascript", false); + response.write("var x = new XMLHttpRequest(); x.open('GET', '" + workerURL + "'); "); + response.write("x.responseType = 'blob'; x.send(); "); + response.write("x.onload = () => { new Worker(URL.createObjectURL(x.response)); };"); + return; + } + + // source for a worker that simply calls importScripts on a script that + // redirects. + if (query["res"] == "makeRequestsWorker") { + // this is code for a worker that imports a redirected script. + let scriptURL = thisSite + resource + "?redir=other&res=script&id=script-src-redir-" + query["id"]; + let xhrURL = thisSite + resource + "?redir=other&res=xhr-resp&id=xhr-src-redir-" + query["id"]; + let fetchURL = thisSite + resource + "?redir=other&res=xhr-resp&id=fetch-src-redir-" + query["id"]; + response.setHeader("Content-Type", "application/javascript", false); + response.write("try { importScripts('" + scriptURL + "'); } catch(ex) {} "); + response.write("var x = new XMLHttpRequest(); x.open('GET', '" + xhrURL + "'); x.send();"); + response.write("fetch('" + fetchURL + "');"); + return; + } + + // script that invokes XHR + if (query["res"] == "xhr") { + response.setHeader("Content-Type", "application/javascript", false); + var resp = 'var x = new XMLHttpRequest();x.open("GET", "' + thisSite + + resource+'?redir=other&res=xhr-resp&id=xhr-src-redir", false);\n' + + 'x.send(null);'; + response.write(resp); + return; + } + + // response to XHR + if (query["res"] == "xhr-resp") { + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Content-Type", "text/html", false); + response.write('XHR response...'); + return; + } +} diff --git a/dom/security/test/csp/file_referrerdirective.html b/dom/security/test/csp/file_referrerdirective.html new file mode 100644 index 000000000..841ffe058 --- /dev/null +++ b/dom/security/test/csp/file_referrerdirective.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Subframe test for bug 965727</title> + +<script type="text/javascript"> +// we can get the ID out of the querystring. +var args = document.location.search.substring(1).split('&'); +var id = "unknown"; +for (var i=0; i < args.length; i++) { + var arg = unescape(args[i]); + if (arg.indexOf('=') > 0 && arg.indexOf('id') == 0) { + id = arg.split('=')[1].trim(); + } +} + +var results = { + 'id': id, + 'referrer': document.location.href, + 'results': { + 'sameorigin': false, + 'crossorigin': false, + 'downgrade': false + } +}; + +// this is called back by each script load. +var postResult = function(loadType, referrerLevel, referrer) { + results.results[loadType] = referrerLevel; + + // and then check if all three have loaded. + for (var id in results.results) { + if (!results.results[id]) { + return; + } + } + //finished if we don't return early + window.parent.postMessage(JSON.stringify(results), "*"); + console.log(JSON.stringify(results)); +} + +</script> +</head> +<body> +Testing ... + +<script src="https://example.com/tests/dom/security/test/csp/referrerdirective.sjs?type=sameorigin&" + onerror="postResult('sameorigin', 'error');"></script> +<script src="https://test2.example.com/tests/dom/security/test/csp/referrerdirective.sjs?type=crossorigin&" + onerror="postResult('crossorigin', 'error');"></script> +<script src="http://example.com/tests/dom/security/test/csp/referrerdirective.sjs?type=downgrade&" + onerror="postResult('downgrade', 'error');"></script> + +</body> +</html> diff --git a/dom/security/test/csp/file_report.html b/dom/security/test/csp/file_report.html new file mode 100644 index 000000000..fb18af805 --- /dev/null +++ b/dom/security/test/csp/file_report.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1033424 - Test csp-report properties </title> + </head> + <body> + <script type="text/javascript"> + var foo = "propEscFoo"; + var bar = "propEscBar"; + // just verifying that we properly escape newlines and quotes + </script> +</body> +</html> diff --git a/dom/security/test/csp/file_report_chromescript.js b/dom/security/test/csp/file_report_chromescript.js new file mode 100644 index 000000000..bf4f70edb --- /dev/null +++ b/dom/security/test/csp/file_report_chromescript.js @@ -0,0 +1,54 @@ +Components.utils.import("resource://gre/modules/Services.jsm"); + +const Ci = Components.interfaces; +const Cc = Components.classes; + +const reportURI = "http://mochi.test:8888/foo.sjs"; + +var openingObserver = { + observe: function(subject, topic, data) { + // subject should be an nsURI + if (subject.QueryInterface == undefined) + return; + + var message = {report: "", error: false}; + + if (topic == 'http-on-opening-request') { + var asciiSpec = subject.QueryInterface(Ci.nsIHttpChannel).URI.asciiSpec; + if (asciiSpec !== reportURI) return; + + var reportText = false; + try { + // Verify that the report was properly formatted. + // We'll parse the report text as JSON and verify that the properties + // have expected values. + var reportText = "{}"; + var uploadStream = subject.QueryInterface(Ci.nsIUploadChannel).uploadStream; + + if (uploadStream) { + // get the bytes from the request body + var binstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); + binstream.setInputStream(uploadStream); + + var segments = []; + for (var count = uploadStream.available(); count; count = uploadStream.available()) { + var data = binstream.readBytes(count); + segments.push(data); + } + + var reportText = segments.join(""); + // rewind stream as we are supposed to - there will be an assertion later if we don't. + uploadStream.QueryInterface(Ci.nsISeekableStream).seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); + } + message.report = reportText; + } catch (e) { + message.error = e.toString(); + } + + sendAsyncMessage('opening-request-completed', message); + Services.obs.removeObserver(openingObserver, 'http-on-opening-request'); + } + } +}; + +Services.obs.addObserver(openingObserver, 'http-on-opening-request', false); diff --git a/dom/security/test/csp/file_report_for_import.css b/dom/security/test/csp/file_report_for_import.css new file mode 100644 index 000000000..b578b77b3 --- /dev/null +++ b/dom/security/test/csp/file_report_for_import.css @@ -0,0 +1 @@ +@import url("http://example.com/tests/dom/security/test/csp/file_report_for_import_server.sjs?stylesheet"); diff --git a/dom/security/test/csp/file_report_for_import.html b/dom/security/test/csp/file_report_for_import.html new file mode 100644 index 000000000..77a36faea --- /dev/null +++ b/dom/security/test/csp/file_report_for_import.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1048048 - Test sending csp-report when using import in css</title> + <link rel="stylesheet" type="text/css" href="file_report_for_import.css"> + </head> + <body> + empty body, just testing @import in the included css for bug 1048048 +</body> +</html> diff --git a/dom/security/test/csp/file_report_for_import_server.sjs b/dom/security/test/csp/file_report_for_import_server.sjs new file mode 100644 index 000000000..e69852b1b --- /dev/null +++ b/dom/security/test/csp/file_report_for_import_server.sjs @@ -0,0 +1,49 @@ +// Custom *.sjs file specifically for the needs of Bug: +// Bug 1048048 - CSP violation report not sent for @import + +const CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + var queryString = request.queryString; + + // (1) lets process the queryresult request async and + // wait till we have received the image request. + if (queryString === "queryresult") { + response.processAsync(); + setObjectState("queryResult", response); + return; + } + + // (2) handle the csp-report and return the JSON back to + // the testfile using the afore stored xml request in (1). + if (queryString === "report") { + getObjectState("queryResult", function(queryResponse) { + if (!queryResponse) { + return; + } + + // send the report back to the XML request for verification + var report = new BinaryInputStream(request.bodyInputStream); + var avail; + var bytes = []; + while ((avail = report.available()) > 0) { + Array.prototype.push.apply(bytes, report.readByteArray(avail)); + } + var data = String.fromCharCode.apply(null, bytes); + queryResponse.bodyOutputStream.write(data, data.length); + queryResponse.finish(); + }); + return; + } + + // we should not get here ever, but just in case return + // something unexpected. + response.write("doh!"); +} diff --git a/dom/security/test/csp/file_report_uri_missing_in_report_only_header.html b/dom/security/test/csp/file_report_uri_missing_in_report_only_header.html new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/dom/security/test/csp/file_report_uri_missing_in_report_only_header.html diff --git a/dom/security/test/csp/file_report_uri_missing_in_report_only_header.html^headers^ b/dom/security/test/csp/file_report_uri_missing_in_report_only_header.html^headers^ new file mode 100644 index 000000000..3f2fdfe9e --- /dev/null +++ b/dom/security/test/csp/file_report_uri_missing_in_report_only_header.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy-Report-Only: default-src 'self'; diff --git a/dom/security/test/csp/file_require_sri_meta.js b/dom/security/test/csp/file_require_sri_meta.js new file mode 100644 index 000000000..af0113297 --- /dev/null +++ b/dom/security/test/csp/file_require_sri_meta.js @@ -0,0 +1 @@ +var foo = 24; diff --git a/dom/security/test/csp/file_require_sri_meta.sjs b/dom/security/test/csp/file_require_sri_meta.sjs new file mode 100644 index 000000000..acaf742db --- /dev/null +++ b/dom/security/test/csp/file_require_sri_meta.sjs @@ -0,0 +1,54 @@ +// custom *.sjs for Bug 1277557 +// META CSP: require-sri-for script; + +const PRE_INTEGRITY = + "<!DOCTYPE HTML>" + + "<html><head><meta charset=\"utf-8\">" + + "<title>Bug 1277557 - CSP require-sri-for does not block when CSP is in meta tag</title>" + + "<meta http-equiv=\"Content-Security-Policy\" content=\"require-sri-for script; script-src 'unsafe-inline' *\">" + + "</head>" + + "<body>" + + "<script id=\"testscript\"" + + // Using math.random() to avoid confusing cache behaviors within the test + " src=\"http://mochi.test:8888/tests/dom/security/test/csp/file_require_sri_meta.js?" + Math.random() + "\""; + +const WRONG_INTEGRITY = + " integrity=\"sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC\""; + +const CORRECT_INEGRITY = + " integrity=\"sha384-PkcuZQHmjBQKRyv1v3x0X8qFmXiSyFyYIP+f9SU86XWvRneifdNCPg2cYFWBuKsF\""; + +const POST_INTEGRITY = + " onload=\"window.parent.postMessage({result: 'script-loaded'}, '*');\"" + + " onerror=\"window.parent.postMessage({result: 'script-blocked'}, '*');\"" + + "></script>" + + "</body>" + + "</html>"; + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + var queryString = request.queryString; + + if (queryString === "no-sri") { + response.write(PRE_INTEGRITY + POST_INTEGRITY); + return; + } + + if (queryString === "wrong-sri") { + response.write(PRE_INTEGRITY + WRONG_INTEGRITY + POST_INTEGRITY); + return; + } + + if (queryString === "correct-sri") { + response.write(PRE_INTEGRITY + CORRECT_INEGRITY + POST_INTEGRITY); + return; + } + + // we should never get here, but just in case + // return something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/csp/file_sandbox_1.html b/dom/security/test/csp/file_sandbox_1.html new file mode 100644 index 000000000..ce1e80c86 --- /dev/null +++ b/dom/security/test/csp/file_sandbox_1.html @@ -0,0 +1,16 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + <!-- sandbox="allow-same-origin" --> + <!-- Content-Security-Policy: default-src 'self' --> + + <!-- these should be stopped by CSP --> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img1_bad&type=img/png"> </img> + + <!-- these should load ok --> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img1a_good&type=img/png" /> + <!-- should not execute script --> + <script src='/tests/dom/security/test/csp/file_sandbox_fail.js'></script> + + </body> +</html> diff --git a/dom/security/test/csp/file_sandbox_10.html b/dom/security/test/csp/file_sandbox_10.html new file mode 100644 index 000000000..f934497ee --- /dev/null +++ b/dom/security/test/csp/file_sandbox_10.html @@ -0,0 +1,12 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + <!-- Content-Security-Policy: default-src 'none'; sandbox --> + + <!-- these should be stopped by CSP --> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img10_bad&type=img/png"> </img> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img10a_bad&type=img/png" /> + <script src='/tests/dom/security/test/csp/file_sandbox_fail.js'></script> + + </body> +</html> diff --git a/dom/security/test/csp/file_sandbox_11.html b/dom/security/test/csp/file_sandbox_11.html new file mode 100644 index 000000000..03697438a --- /dev/null +++ b/dom/security/test/csp/file_sandbox_11.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> <meta charset="utf-8"> </head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.postMessage({ok: result, desc: desc}, "*"); + } + + function doStuff() { + ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts"); + } +</script> +<script src='file_sandbox_fail.js'></script> +<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'> + I am sandboxed but with only inline "allow-scripts" + + <!-- Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline'; sandbox allow-scripts --> + + <!-- these should be stopped by CSP --> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img11_bad&type=img/png" /> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img11a_bad&type=img/png"> </img> + <script src='/tests/dom/security/test/csp/file_CSP.sjs?testid=script11_bad&type=text/javascript'></script> + <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script11a_bad&type=text/javascript'></script> +</body> +</html> diff --git a/dom/security/test/csp/file_sandbox_12.html b/dom/security/test/csp/file_sandbox_12.html new file mode 100644 index 000000000..8bed9dc59 --- /dev/null +++ b/dom/security/test/csp/file_sandbox_12.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.postMessage({ok: result, desc: desc}, "*"); + } + + function doStuff() { + ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts"); + + document.getElementById('a_form').submit(); + + // trigger the javascript: url test + sendMouseEvent({type:'click'}, 'a_link'); + } +</script> +<script src='file_sandbox_pass.js'></script> +<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'> + I am sandboxed but with "allow-same-origin" and allow-scripts" + + + <!-- Content-Security-Policy: sandbox allow-same-origin allow-scripts; default-src 'self' 'unsafe-inline'; --> + + <!-- these should be stopped by CSP --> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img12_bad&type=img/png"> </img> + <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script12_bad&type=text/javascript'></script> + + <form method="get" action="/tests/content/html/content/test/file_iframe_sandbox_form_fail.html" id="a_form"> + First name: <input type="text" name="firstname"> + Last name: <input type="text" name="lastname"> + <input type="submit" onclick="doSubmit()" id="a_button"> + </form> + + <a href = 'javascript:ok(true, "documents sandboxed with allow-scripts should be able to run script from javascript: URLs");' id='a_link'>click me</a> +</body> +</html> diff --git a/dom/security/test/csp/file_sandbox_13.html b/dom/security/test/csp/file_sandbox_13.html new file mode 100644 index 000000000..e4672ed05 --- /dev/null +++ b/dom/security/test/csp/file_sandbox_13.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> <meta charset="utf-8"> </head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.postMessage({ok: result, desc: desc}, "*"); + } + + function doStuff() { + ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts"); + } +</script> +<script src='file_sandbox_fail.js'></script> +<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'> + I am sandboxed but with only inline "allow-scripts" + + <!-- Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline'; sandbox allow-scripts --> + + <!-- these should be stopped by CSP --> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img13_bad&type=img/png" /> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img13a_bad&type=img/png"> </img> + <script src='/tests/dom/security/test/csp/file_CSP.sjs?testid=script13_bad&type=text/javascript'></script> + <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script13a_bad&type=text/javascript'></script> +</body> +</html> diff --git a/dom/security/test/csp/file_sandbox_2.html b/dom/security/test/csp/file_sandbox_2.html new file mode 100644 index 000000000..b37aa1bce --- /dev/null +++ b/dom/security/test/csp/file_sandbox_2.html @@ -0,0 +1,16 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + <!-- sandbox --> + <!-- Content-Security-Policy: default-src 'self' --> + + <!-- these should be stopped by CSP --> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img2_bad&type=img/png"> </img> + + <!-- these should load ok --> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img2a_good&type=img/png" /> + <!-- should not execute script --> + <script src='/tests/dom/security/test/csp/file_sandbox_fail.js'></script> + + </body> +</html> diff --git a/dom/security/test/csp/file_sandbox_3.html b/dom/security/test/csp/file_sandbox_3.html new file mode 100644 index 000000000..ba808e47d --- /dev/null +++ b/dom/security/test/csp/file_sandbox_3.html @@ -0,0 +1,13 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + <!-- sandbox="allow-same-origin" --> + <!-- Content-Security-Policy: default-src 'none' --> + + <!-- these should be stopped by CSP --> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img3_bad&type=img/png"> </img> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img3a_bad&type=img/png" /> + <script src='/tests/dom/security/test/csp/file_sandbox_fail.js'></script> + + </body> +</html> diff --git a/dom/security/test/csp/file_sandbox_4.html b/dom/security/test/csp/file_sandbox_4.html new file mode 100644 index 000000000..b2d4ed094 --- /dev/null +++ b/dom/security/test/csp/file_sandbox_4.html @@ -0,0 +1,13 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + <!-- sandbox --> + <!-- Content-Security-Policy: default-src 'none' --> + + <!-- these should be stopped by CSP --> + <img src="http://example.org/tests/dom/base/test/csp/file_CSP.sjs?testid=img4_bad&type=img/png"> </img> + <img src="/tests/dom/base/test/csp/file_CSP.sjs?testid=img4a_bad&type=img/png" /> + <script src='/tests/dom/base/test/csp/file_sandbox_fail.js'></script> + + </body> +</html> diff --git a/dom/security/test/csp/file_sandbox_5.html b/dom/security/test/csp/file_sandbox_5.html new file mode 100644 index 000000000..79894eabb --- /dev/null +++ b/dom/security/test/csp/file_sandbox_5.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> <meta charset="utf-8"> </head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.postMessage({ok: result, desc: desc}, "*"); + } + + function doStuff() { + ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts"); + } +</script> +<script src='file_sandbox_fail.js'></script> +<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'> + I am sandboxed but with only inline "allow-scripts" + + <!-- sandbox="allow-scripts" --> + <!-- Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline' --> + + <!-- these should be stopped by CSP --> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img5_bad&type=img/png" /> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img5a_bad&type=img/png"> </img> + <script src='/tests/dom/security/test/csp/file_CSP.sjs?testid=script5_bad&type=text/javascript'></script> + <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script5a_bad&type=text/javascript'></script> +</body> +</html> diff --git a/dom/security/test/csp/file_sandbox_6.html b/dom/security/test/csp/file_sandbox_6.html new file mode 100644 index 000000000..c23d87860 --- /dev/null +++ b/dom/security/test/csp/file_sandbox_6.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.postMessage({ok: result, desc: desc}, "*"); + } + + function doStuff() { + ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts"); + + document.getElementById('a_form').submit(); + + // trigger the javascript: url test + sendMouseEvent({type:'click'}, 'a_link'); + } +</script> +<script src='file_sandbox_pass.js'></script> +<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'> + I am sandboxed but with "allow-same-origin" and allow-scripts" + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img6_bad&type=img/png"> </img> + <script src='http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=script6_bad&type=text/javascript'></script> + + <form method="get" action="/tests/content/html/content/test/file_iframe_sandbox_form_fail.html" id="a_form"> + First name: <input type="text" name="firstname"> + Last name: <input type="text" name="lastname"> + <input type="submit" onclick="doSubmit()" id="a_button"> + </form> + + <a href = 'javascript:ok(true, "documents sandboxed with allow-scripts should be able to run script from javascript: URLs");' id='a_link'>click me</a> +</body> +</html> diff --git a/dom/security/test/csp/file_sandbox_7.html b/dom/security/test/csp/file_sandbox_7.html new file mode 100644 index 000000000..3b249d410 --- /dev/null +++ b/dom/security/test/csp/file_sandbox_7.html @@ -0,0 +1,15 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + <!-- Content-Security-Policy: default-src 'self'; sandbox allow-same-origin --> + + <!-- these should be stopped by CSP --> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img7_bad&type=img/png"> </img> + + <!-- these should load ok --> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img7a_good&type=img/png" /> + <!-- should not execute script --> + <script src='/tests/dom/security/test/csp/file_sandbox_fail.js'></script> + + </body> +</html> diff --git a/dom/security/test/csp/file_sandbox_8.html b/dom/security/test/csp/file_sandbox_8.html new file mode 100644 index 000000000..4f9cd8916 --- /dev/null +++ b/dom/security/test/csp/file_sandbox_8.html @@ -0,0 +1,15 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + <!-- Content-Security-Policy: sandbox; default-src 'self' --> + + <!-- these should be stopped by CSP --> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img8_bad&type=img/png"> </img> + + <!-- these should load ok --> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img8a_good&type=img/png" /> + <!-- should not execute script --> + <script src='/tests/dom/security/test/csp/file_sandbox_fail.js'></script> + + </body> +</html> diff --git a/dom/security/test/csp/file_sandbox_9.html b/dom/security/test/csp/file_sandbox_9.html new file mode 100644 index 000000000..29ffc191c --- /dev/null +++ b/dom/security/test/csp/file_sandbox_9.html @@ -0,0 +1,12 @@ +<html> +<head> <meta charset="utf-8"> </head> + <body> + <!-- Content-Security-Policy: default-src 'none'; sandbox allow-same-origin --> + + <!-- these should be stopped by CSP --> + <img src="http://example.org/tests/dom/security/test/csp/file_CSP.sjs?testid=img9_bad&type=img/png"> </img> + <img src="/tests/dom/security/test/csp/file_CSP.sjs?testid=img9a_bad&type=img/png" /> + + <script src='/tests/dom/security/test/csp/file_sandbox_fail.js'></script> + </body> +</html> diff --git a/dom/security/test/csp/file_sandbox_allow_scripts.html b/dom/security/test/csp/file_sandbox_allow_scripts.html new file mode 100644 index 000000000..faab9f0fc --- /dev/null +++ b/dom/security/test/csp/file_sandbox_allow_scripts.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'> + <title>Bug 1396320: Fix CSP sandbox regression for allow-scripts</title> + </head> +<body> +<script type='application/javascript'> + window.parent.postMessage({result: document.domain }, '*'); +</script> +</body> +</html> diff --git a/dom/security/test/csp/file_sandbox_allow_scripts.html^headers^ b/dom/security/test/csp/file_sandbox_allow_scripts.html^headers^ new file mode 100644 index 000000000..4705ce9de --- /dev/null +++ b/dom/security/test/csp/file_sandbox_allow_scripts.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: sandbox allow-scripts; diff --git a/dom/security/test/csp/file_sandbox_fail.js b/dom/security/test/csp/file_sandbox_fail.js new file mode 100644 index 000000000..5403ff218 --- /dev/null +++ b/dom/security/test/csp/file_sandbox_fail.js @@ -0,0 +1,4 @@ +function ok(result, desc) { + window.parent.postMessage({ok: result, desc: desc}, "*"); +} +ok(false, "documents sandboxed with allow-scripts should NOT be able to run <script src=...>"); diff --git a/dom/security/test/csp/file_sandbox_pass.js b/dom/security/test/csp/file_sandbox_pass.js new file mode 100644 index 000000000..2fd0350dc --- /dev/null +++ b/dom/security/test/csp/file_sandbox_pass.js @@ -0,0 +1,4 @@ +function ok(result, desc) { + window.parent.postMessage({ok: result, desc: desc}, "*"); +} +ok(true, "documents sandboxed with allow-scripts should be able to run <script src=...>"); diff --git a/dom/security/test/csp/file_scheme_relative_sources.js b/dom/security/test/csp/file_scheme_relative_sources.js new file mode 100644 index 000000000..09286d42e --- /dev/null +++ b/dom/security/test/csp/file_scheme_relative_sources.js @@ -0,0 +1 @@ +document.getElementById("testdiv").innerHTML = "allowed"; diff --git a/dom/security/test/csp/file_scheme_relative_sources.sjs b/dom/security/test/csp/file_scheme_relative_sources.sjs new file mode 100644 index 000000000..8c4d62ca5 --- /dev/null +++ b/dom/security/test/csp/file_scheme_relative_sources.sjs @@ -0,0 +1,42 @@ +/** + * Custom *.sjs specifically for the needs of + * Bug 921493 - CSP: test whitelisting of scheme-relative sources + */ + +function handleRequest(request, response) +{ + Components.utils.importGlobalProperties(["URLSearchParams"]); + let query = new URLSearchParams(request.queryString); + + let scheme = query.get("scheme"); + let policy = query.get("policy"); + + let linkUrl = scheme + + "://example.com/tests/dom/security/test/csp/file_scheme_relative_sources.js"; + + let html = "<!DOCTYPE HTML>" + + "<html>" + + "<head>" + + "<title>test schemeless sources within CSP</title>" + + "</head>" + + "<body> " + + "<div id='testdiv'>blocked</div>" + + // try to load a scheme relative script + "<script src='" + linkUrl + "'></script>" + + // have an inline script that reports back to the parent whether + // the script got loaded or not from within the sandboxed iframe. + "<script type='application/javascript'>" + + "window.onload = function() {" + + "var inner = document.getElementById('testdiv').innerHTML;" + + "window.parent.postMessage({ result: inner }, '*');" + + "}" + + "</script>" + + "</body>" + + "</html>"; + + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Security-Policy", policy, false); + + response.write(html); +} diff --git a/dom/security/test/csp/file_self_none_as_hostname_confusion.html b/dom/security/test/csp/file_self_none_as_hostname_confusion.html new file mode 100644 index 000000000..16196bb19 --- /dev/null +++ b/dom/security/test/csp/file_self_none_as_hostname_confusion.html @@ -0,0 +1,11 @@ +<!doctype html> +<html> + <head> + <meta charset="utf8"> + <title>Bug 587377 - CSP keywords "'self'" and "'none'" are easy to confuse with host names "self" and "none"</title> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + </body> +</html> diff --git a/dom/security/test/csp/file_self_none_as_hostname_confusion.html^headers^ b/dom/security/test/csp/file_self_none_as_hostname_confusion.html^headers^ new file mode 100644 index 000000000..26af7ed9b --- /dev/null +++ b/dom/security/test/csp/file_self_none_as_hostname_confusion.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' SELF; diff --git a/dom/security/test/csp/file_sendbeacon.html b/dom/security/test/csp/file_sendbeacon.html new file mode 100644 index 000000000..13202c65f --- /dev/null +++ b/dom/security/test/csp/file_sendbeacon.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content= "connect-src 'none'"> + <title>Bug 1234813 - sendBeacon should not throw if blocked by Content Policy</title> +</head> +<body> + +<script type="application/javascript"> +try { + navigator.sendBeacon("http://example.com/sendbeaconintonirvana"); + window.parent.postMessage({result: "blocked-beacon-does-not-throw"}, "*"); + } + catch (e) { + window.parent.postMessage({result: "blocked-beacon-throws"}, "*"); + } +</script> + +</body> +</html> diff --git a/dom/security/test/csp/file_service_worker.html b/dom/security/test/csp/file_service_worker.html new file mode 100644 index 000000000..00a2b4020 --- /dev/null +++ b/dom/security/test/csp/file_service_worker.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1208559 - ServiceWorker registration not governed by CSP</title> +</head> +<body> +<script> + function finish(status) { + window.parent.postMessage({result: status}, "*"); + } + + navigator.serviceWorker.ready.then(finish.bind(null, 'allowed'), + finish.bind(null, 'blocked')); + navigator.serviceWorker + .register("file_service_worker.js", {scope: "."}) + .then(null, finish.bind(null, 'blocked')); + </script> +</body> +</html> diff --git a/dom/security/test/csp/file_service_worker.js b/dom/security/test/csp/file_service_worker.js new file mode 100644 index 000000000..1bf583f4c --- /dev/null +++ b/dom/security/test/csp/file_service_worker.js @@ -0,0 +1 @@ +dump("service workers: hello world"); diff --git a/dom/security/test/csp/file_shouldprocess.html b/dom/security/test/csp/file_shouldprocess.html new file mode 100644 index 000000000..0684507e9 --- /dev/null +++ b/dom/security/test/csp/file_shouldprocess.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Helper for Test Bug 908933</title> + <meta charset="utf-8"> + </head> + <body> + <object type="application/x-java-test" codebase="test1"></object> + + <object classid="java:test2" codebase="./test2"></object> + + <object data="test3" classid="java:test3" codebase="./test3"></object> + + <applet codebase="test4"></applet> + + <embed src="test5.class" codebase="test5" type="application/x-java-test"> + + <embed type="application/x-java-test" codebase="test6"> + + <embed src="test7.class"> + + <embed src="test8.class" codebase="test8"> + + </body> +</html> diff --git a/dom/security/test/csp/file_strict_dynamic.js b/dom/security/test/csp/file_strict_dynamic.js new file mode 100644 index 000000000..09286d42e --- /dev/null +++ b/dom/security/test/csp/file_strict_dynamic.js @@ -0,0 +1 @@ +document.getElementById("testdiv").innerHTML = "allowed"; diff --git a/dom/security/test/csp/file_strict_dynamic_default_src.html b/dom/security/test/csp/file_strict_dynamic_default_src.html new file mode 100644 index 000000000..35feacb4f --- /dev/null +++ b/dom/security/test/csp/file_strict_dynamic_default_src.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title> +</head> +<body> + +<div id="testdiv">blocked</div> +<script nonce="foo" src="http://mochi.test:8888/tests/dom/security/test/csp/file_strict_dynamic_default_src.js"></script> + +<img id="testimage" src="http://mochi.test:8888/tests/image/test/mochitest/blue.png"></img> + +</body> +</html> diff --git a/dom/security/test/csp/file_strict_dynamic_default_src.js b/dom/security/test/csp/file_strict_dynamic_default_src.js new file mode 100644 index 000000000..09286d42e --- /dev/null +++ b/dom/security/test/csp/file_strict_dynamic_default_src.js @@ -0,0 +1 @@ +document.getElementById("testdiv").innerHTML = "allowed"; diff --git a/dom/security/test/csp/file_strict_dynamic_js_url.html b/dom/security/test/csp/file_strict_dynamic_js_url.html new file mode 100644 index 000000000..bd53b0adb --- /dev/null +++ b/dom/security/test/csp/file_strict_dynamic_js_url.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1316826 - 'strict-dynamic' blocking DOM event handlers</title> +</head> +<body> +<div id="testdiv">blocked</div> + +<a id="jslink" href='javascript:document.getElementById("testdiv").innerHTML = "allowed"'>click me</a> +<script nonce="foo"> + document.getElementById("jslink").click(); +</script> + +</body> +</html> diff --git a/dom/security/test/csp/file_strict_dynamic_non_parser_inserted.html b/dom/security/test/csp/file_strict_dynamic_non_parser_inserted.html new file mode 100644 index 000000000..c51fefd72 --- /dev/null +++ b/dom/security/test/csp/file_strict_dynamic_non_parser_inserted.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title> +</head> +<body> +<div id="testdiv">blocked</div> + +<script nonce="foo"> + // generates a *non* parser inserted script and should be allowed + var myScript = document.createElement('script'); + myScript.src = 'http://example.com/tests/dom/security/test/csp/file_strict_dynamic.js'; + document.head.appendChild(myScript); +</script> + +</body> +</html> diff --git a/dom/security/test/csp/file_strict_dynamic_non_parser_inserted_inline.html b/dom/security/test/csp/file_strict_dynamic_non_parser_inserted_inline.html new file mode 100644 index 000000000..10a0f32e4 --- /dev/null +++ b/dom/security/test/csp/file_strict_dynamic_non_parser_inserted_inline.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title> +</head> +<body> +<div id="testdiv">blocked</div> + +<script nonce="foo"> + var dynamicScript = document.createElement('script'); + dynamicScript.textContent = 'document.getElementById("testdiv").textContent="allowed"'; + document.head.appendChild(dynamicScript); +</script> + +</body> +</html> diff --git a/dom/security/test/csp/file_strict_dynamic_parser_inserted_doc_write.html b/dom/security/test/csp/file_strict_dynamic_parser_inserted_doc_write.html new file mode 100644 index 000000000..2a3a7d499 --- /dev/null +++ b/dom/security/test/csp/file_strict_dynamic_parser_inserted_doc_write.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title> +</head> +<body> +<div id="testdiv">blocked</div> + +<script nonce="foo"> + // generates a parser inserted script and should be blocked + document.write("<script src='http://example.com/tests/dom/security/test/csp/file_strict_dynamic.js'><\/script>"); +</script> + +</body> +</html> diff --git a/dom/security/test/csp/file_strict_dynamic_parser_inserted_doc_write_correct_nonce.html b/dom/security/test/csp/file_strict_dynamic_parser_inserted_doc_write_correct_nonce.html new file mode 100644 index 000000000..9938ef2dc --- /dev/null +++ b/dom/security/test/csp/file_strict_dynamic_parser_inserted_doc_write_correct_nonce.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title> +</head> +<body> +<div id="testdiv">blocked</div> + +<script nonce="foo"> + // generates a parser inserted script with a valid nonce- and should be allowed + document.write("<script nonce='foo' src='http://example.com/tests/dom/security/test/csp/file_strict_dynamic.js'><\/script>"); +</script> + +</body> +</html> diff --git a/dom/security/test/csp/file_strict_dynamic_script_events.html b/dom/security/test/csp/file_strict_dynamic_script_events.html new file mode 100644 index 000000000..088958382 --- /dev/null +++ b/dom/security/test/csp/file_strict_dynamic_script_events.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1316826 - 'strict-dynamic' blocking DOM event handlers</title> +</head> +<body> +<div id="testdiv">blocked</div> + + <img src='/nonexisting.jpg' + onerror='document.getElementById("testdiv").innerHTML = "allowed";' + style='display:none'> + +</body> +</html> diff --git a/dom/security/test/csp/file_strict_dynamic_script_events_xbl.html b/dom/security/test/csp/file_strict_dynamic_script_events_xbl.html new file mode 100644 index 000000000..701ef3226 --- /dev/null +++ b/dom/security/test/csp/file_strict_dynamic_script_events_xbl.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1316826 - 'strict-dynamic' blocking DOM event handlers</title> +</head> +<body> +<div id="testdiv">blocked</div> + +<marquee onstart='document.getElementById("testdiv").innerHTML = "allowed";'> + Bug 1316826 +</marquee> + +</body> +</html> diff --git a/dom/security/test/csp/file_strict_dynamic_script_extern.html b/dom/security/test/csp/file_strict_dynamic_script_extern.html new file mode 100644 index 000000000..94b6aefb1 --- /dev/null +++ b/dom/security/test/csp/file_strict_dynamic_script_extern.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title> +</head> +<body> +<div id="testdiv">blocked</div> +<script nonce="foo" src="http://example.com/tests/dom/security/test/csp/file_strict_dynamic.js"></script> +</body> +</html> diff --git a/dom/security/test/csp/file_strict_dynamic_script_inline.html b/dom/security/test/csp/file_strict_dynamic_script_inline.html new file mode 100644 index 000000000..d17a58f27 --- /dev/null +++ b/dom/security/test/csp/file_strict_dynamic_script_inline.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title> +</head> +<body> +<div id="testdiv">blocked</div> + +<script nonce="foo"> + document.getElementById("testdiv").innerHTML = "allowed"; +</script> + +</body> +</html> diff --git a/dom/security/test/csp/file_strict_dynamic_unsafe_eval.html b/dom/security/test/csp/file_strict_dynamic_unsafe_eval.html new file mode 100644 index 000000000..f0b26da91 --- /dev/null +++ b/dom/security/test/csp/file_strict_dynamic_unsafe_eval.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title> +</head> +<body> +<div id="testdiv">blocked</div> + +<script nonce="foo"> + eval('document.getElementById("testdiv").innerHTML = "allowed";'); +</script> + +</body> +</html>
\ No newline at end of file diff --git a/dom/security/test/csp/file_subframe_run_js_if_allowed.html b/dom/security/test/csp/file_subframe_run_js_if_allowed.html new file mode 100644 index 000000000..3ba970ce8 --- /dev/null +++ b/dom/security/test/csp/file_subframe_run_js_if_allowed.html @@ -0,0 +1,13 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=702439 + +This document is a child frame of a CSP document and the +test verifies that it is permitted to run javascript: URLs +if the parent has a policy that allows them. +--> +<body onload="document.getElementById('a').click()"> +<a id="a" href="javascript:parent.javascript_link_ran = true; + parent.checkResult();">click</a> +</body> +</html> diff --git a/dom/security/test/csp/file_subframe_run_js_if_allowed.html^headers^ b/dom/security/test/csp/file_subframe_run_js_if_allowed.html^headers^ new file mode 100644 index 000000000..233b35931 --- /dev/null +++ b/dom/security/test/csp/file_subframe_run_js_if_allowed.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src *; script-src 'unsafe-inline' diff --git a/dom/security/test/csp/file_testserver.sjs b/dom/security/test/csp/file_testserver.sjs new file mode 100644 index 000000000..ae1826611 --- /dev/null +++ b/dom/security/test/csp/file_testserver.sjs @@ -0,0 +1,57 @@ +// SJS file for CSP mochitests +"use strict"; +Components.utils.import("resource://gre/modules/NetUtil.jsm"); +Components.utils.importGlobalProperties(["URLSearchParams"]); + +function loadHTMLFromFile(path) { + // Load the HTML to return in the response from file. + // Since it's relative to the cwd of the test runner, we start there and + // append to get to the actual path of the file. + const testHTMLFile = + Components.classes["@mozilla.org/file/directory_service;1"]. + getService(Components.interfaces.nsIProperties). + get("CurWorkD", Components.interfaces.nsILocalFile); + + const testHTMLFileStream = + Components.classes["@mozilla.org/network/file-input-stream;1"]. + createInstance(Components.interfaces.nsIFileInputStream); + + path + .split("/") + .filter(path => path) + .reduce((file, path) => { + testHTMLFile.append(path) + return testHTMLFile; + }, testHTMLFile); + testHTMLFileStream.init(testHTMLFile, -1, 0, 0); + const isAvailable = testHTMLFileStream.available(); + return NetUtil.readInputStreamToString(testHTMLFileStream, isAvailable); +} + +function handleRequest(request, response) { + const query = new URLSearchParams(request.queryString); + + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // Deliver the CSP policy encoded in the URL + if(query.has("csp")){ + response.setHeader("Content-Security-Policy", query.get("csp"), false); + } + + // Deliver the CSP report-only policy encoded in the URI + if(query.has("cspRO")){ + response.setHeader("Content-Security-Policy-Report-Only", query.get("cspRO"), false); + } + + // Deliver the CORS header in the URL + if(query.has("cors")){ + response.setHeader("Access-Control-Allow-Origin", query.get("cors"), false); + } + + // Send HTML to test allowed/blocked behaviors + response.setHeader("Content-Type", "text/html", false); + if(query.has("file")){ + response.write(loadHTMLFromFile(query.get("file"))); + } +} diff --git a/dom/security/test/csp/file_upgrade_insecure.html b/dom/security/test/csp/file_upgrade_insecure.html new file mode 100644 index 000000000..d104a3a24 --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title> + <!-- style --> + <link rel='stylesheet' type='text/css' href='http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?style' media='screen' /> + + <!-- font --> + <style> + @font-face { + font-family: "foofont"; + src: url('http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?font'); + } + .div_foo { font-family: "foofont"; } + </style> +</head> +<body> + + <!-- images: --> + <img src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?img"></img> + + <!-- redirects: upgrade http:// to https:// redirect to http:// and then upgrade to https:// again --> + <img src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?redirect-image"></img> + + <!-- script: --> + <script src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?script"></script> + + <!-- media: --> + <audio src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?media"></audio> + + <!-- objects: --> + <object width="10" height="10" data="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?object"></object> + + <!-- font: (apply font loaded in header to div) --> + <div class="div_foo">foo</div> + + <!-- iframe: (same origin) --> + <iframe src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?iframe"> + <!-- within that iframe we load an image over http and make sure the requested gets upgraded to https --> + </iframe> + + <!-- xhr: --> + <script type="application/javascript"> + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?xhr"); + myXHR.send(null); + </script> + + <!-- websockets: upgrade ws:// to wss://--> + <script type="application/javascript"> + var mySocket = new WebSocket("ws://example.com/tests/dom/security/test/csp/file_upgrade_insecure"); + mySocket.onopen = function(e) { + if (mySocket.url.includes("wss://")) { + window.parent.postMessage({result: "websocket-ok"}, "*"); + } + else { + window.parent.postMessage({result: "websocket-error"}, "*"); + } + }; + mySocket.onerror = function(e) { + window.parent.postMessage({result: "websocket-unexpected-error"}, "*"); + }; + </script> + + <!-- form action: (upgrade POST from http:// to https://) --> + <iframe name='formFrame' id='formFrame'></iframe> + <form target="formFrame" action="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?form" method="POST"> + <input name="foo" value="foo"> + <input type="submit" id="submitButton" formenctype='multipart/form-data' value="Submit form"> + </form> + <script type="text/javascript"> + var submitButton = document.getElementById('submitButton'); + submitButton.click(); + </script> + +</body> +</html> diff --git a/dom/security/test/csp/file_upgrade_insecure_cors.html b/dom/security/test/csp/file_upgrade_insecure_cors.html new file mode 100644 index 000000000..e675c62e9 --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_cors.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title> +</head> +<body> + +<script type="text/javascript"> + // === TEST 1 + var url1 = "http://test1.example.com/tests/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs?test1"; + var xhr1 = new XMLHttpRequest(); + xhr1.open("GET", url1, true); + xhr1.onload = function() { + window.parent.postMessage(xhr1.response, "*"); + }; + xhr1.onerror = function() { + window.parent.postMessage("test1-failed", "*"); + }; + xhr1.send(); + + // === TEST 2 + var url2 = "http://test1.example.com/tests/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs?test2"; + var xhr2 = new XMLHttpRequest(); + xhr2.open("GET", url2, true); + xhr2.onload = function() { + window.parent.postMessage(xhr2.response, "*"); + }; + xhr2.onerror = function() { + window.parent.postMessage("test2-failed", "*"); + }; + xhr2.send(); + + // === TEST 3 + var url3 = "http://test2.example.com/tests/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs?test3"; + var xhr3 = new XMLHttpRequest(); + xhr3.open("GET", url3, true); + xhr3.onload = function() { + window.parent.postMessage(xhr3.response, "*"); + }; + xhr3.onerror = function() { + window.parent.postMessage("test3-failed", "*"); + }; + xhr3.send(); + +</script> + +</body> +</html> diff --git a/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs b/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs new file mode 100644 index 000000000..33f6c3b23 --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs @@ -0,0 +1,62 @@ +// Custom *.sjs file specifically for the needs of Bug: +// Bug 1139297 - Implement CSP upgrade-insecure-requests directive + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // perform sanity check and make sure that all requests get upgraded to use https + if (request.scheme !== "https") { + response.write("request not https"); + return; + } + + var queryString = request.queryString; + + // TEST 1 + if (queryString === "test1") { + var newLocation = + "http://test1.example.com/tests/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs?redir-test1"; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", newLocation, false); + return; + } + if (queryString === "redir-test1") { + response.write("test1-no-cors-ok"); + return; + } + + // TEST 2 + if (queryString === "test2") { + var newLocation = + "http://test1.example.com:443/tests/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs?redir-test2"; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", newLocation, false); + return; + } + if (queryString === "redir-test2") { + response.write("test2-no-cors-diffport-ok"); + return; + } + + // TEST 3 + response.setHeader("Access-Control-Allow-Headers", "content-type", false); + response.setHeader("Access-Control-Allow-Methods", "POST, GET", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + + if (queryString === "test3") { + var newLocation = + "http://test1.example.com/tests/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs?redir-test3"; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", newLocation, false); + return; + } + if (queryString === "redir-test3") { + response.write("test3-cors-ok"); + return; + } + + // we should not get here, but just in case return something unexpected + response.write("d'oh"); +} diff --git a/dom/security/test/csp/file_upgrade_insecure_docwrite_iframe.sjs b/dom/security/test/csp/file_upgrade_insecure_docwrite_iframe.sjs new file mode 100644 index 000000000..6870a57bb --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_docwrite_iframe.sjs @@ -0,0 +1,54 @@ +// custom *.sjs for Bug 1273430 +// META CSP: upgrade-insecure-requests + +// important: the IFRAME_URL is *http* and needs to be upgraded to *https* by upgrade-insecure-requests +const IFRAME_URL = + "http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_docwrite_iframe.sjs?docwriteframe"; + +const TEST_FRAME = ` + <!DOCTYPE HTML> + <html><head><meta charset="utf-8"> + <title>TEST_FRAME</title> + <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> + </head> + <body> + <script type="text/javascript"> + document.write('<iframe src="` + IFRAME_URL + `"/>'); + </script> + </body> + </html>`; + + +// doc.write(iframe) sends a post message to the parent indicating the current +// location so the parent can make sure the request was upgraded to *https*. +const DOC_WRITE_FRAME = ` + <!DOCTYPE HTML> + <html><head><meta charset="utf-8"> + <title>DOC_WRITE_FRAME</title> + </head> + <body onload="window.parent.parent.postMessage({result: document.location.href}, '*');"> + </body> + </html>`; + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + var queryString = request.queryString; + + if (queryString === "testframe") { + response.write(TEST_FRAME); + return; + } + + if (queryString === "docwriteframe") { + response.write(DOC_WRITE_FRAME); + return; + } + + // we should never get here, but just in case + // return something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/csp/file_upgrade_insecure_meta.html b/dom/security/test/csp/file_upgrade_insecure_meta.html new file mode 100644 index 000000000..5f65e78ec --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_meta.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests; default-src https: wss: 'unsafe-inline'; form-action https:;"> + <meta charset="utf-8"> + <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title> + <!-- style --> + <link rel='stylesheet' type='text/css' href='http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?style' media='screen' /> + + <!-- font --> + <style> + @font-face { + font-family: "foofont"; + src: url('http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?font'); + } + .div_foo { font-family: "foofont"; } + </style> +</head> +<body> + + <!-- images: --> + <img src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?img"></img> + + <!-- redirects: upgrade http:// to https:// redirect to http:// and then upgrade to https:// again --> + <img src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?redirect-image"></img> + + <!-- script: --> + <script src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?script"></script> + + <!-- media: --> + <audio src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?media"></audio> + + <!-- objects: --> + <object width="10" height="10" data="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?object"></object> + + <!-- font: (apply font loaded in header to div) --> + <div class="div_foo">foo</div> + + <!-- iframe: (same origin) --> + <iframe src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?iframe"> + <!-- within that iframe we load an image over http and make sure the requested gets upgraded to https --> + </iframe> + + <!-- xhr: --> + <script type="application/javascript"> + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?xhr"); + myXHR.send(null); + </script> + + <!-- websockets: upgrade ws:// to wss://--> + <script type="application/javascript"> + var mySocket = new WebSocket("ws://example.com/tests/dom/security/test/csp/file_upgrade_insecure"); + mySocket.onopen = function(e) { + if (mySocket.url.includes("wss://")) { + window.parent.postMessage({result: "websocket-ok"}, "*"); + } + else { + window.parent.postMessage({result: "websocket-error"}, "*"); + } + }; + mySocket.onerror = function(e) { + window.parent.postMessage({result: "websocket-unexpected-error"}, "*"); + }; + </script> + + <!-- form action: (upgrade POST from http:// to https://) --> + <iframe name='formFrame' id='formFrame'></iframe> + <form target="formFrame" action="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?form" method="POST"> + <input name="foo" value="foo"> + <input type="submit" id="submitButton" formenctype='multipart/form-data' value="Submit form"> + </form> + <script type="text/javascript"> + var submitButton = document.getElementById('submitButton'); + submitButton.click(); + </script> + +</body> +</html> diff --git a/dom/security/test/csp/file_upgrade_insecure_referrer.sjs b/dom/security/test/csp/file_upgrade_insecure_referrer.sjs new file mode 100644 index 000000000..e149afa4b --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_referrer.sjs @@ -0,0 +1,55 @@ +// special *.sjs specifically customized for the needs of +// Bug 1139297 and Bug 663570 + +const PRE_HEAD = + "<!DOCTYPE HTML>" + + "<html>" + + "<head>"; + + const POST_HEAD = + "<meta charset='utf-8'>" + + "<title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title>" + + "</head>" + + "<body>" + + "<img id='testimage' src='http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_referrer_server.sjs?img'></img>" + + "</body>" + + "</html>"; + +const PRE_CSP = "upgrade-insecure-requests; default-src https:; "; +const CSP_REFERRER_ORIGIN = "referrer origin"; +const CSP_REFEFFER_NO_REFERRER = "referrer no-referrer"; + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + var queryString = request.queryString; + + if (queryString === "test1") { + response.setHeader("Content-Security-Policy", PRE_CSP + CSP_REFERRER_ORIGIN, false); + response.write(PRE_HEAD + POST_HEAD); + return; + } + + if (queryString === "test2") { + response.setHeader("Content-Security-Policy", PRE_CSP + CSP_REFEFFER_NO_REFERRER, false); + response.write(PRE_HEAD + POST_HEAD); + return; + } + + if (queryString === "test3") { + var metacsp = "<meta http-equiv=\"Content-Security-Policy\" content = \"" + PRE_CSP + CSP_REFERRER_ORIGIN + "\" >"; + response.write(PRE_HEAD + metacsp + POST_HEAD); + return; + } + + if (queryString === "test4") { + var metacsp = "<meta http-equiv=\"Content-Security-Policy\" content = \"" + PRE_CSP + CSP_REFEFFER_NO_REFERRER + "\" >"; + response.write(PRE_HEAD + metacsp + POST_HEAD); + return; + } + + // we should never get here, but just in case return + // something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/csp/file_upgrade_insecure_referrer_server.sjs b/dom/security/test/csp/file_upgrade_insecure_referrer_server.sjs new file mode 100644 index 000000000..be1e6da0c --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_referrer_server.sjs @@ -0,0 +1,56 @@ +// Custom *.sjs file specifically for the needs of Bug: +// Bug 1139297 - Implement CSP upgrade-insecure-requests directive + +// small red image +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="); + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + var queryString = request.queryString; + + // (1) lets process the queryresult request async and + // wait till we have received the image request. + if (queryString == "queryresult") { + response.processAsync(); + setObjectState("queryResult", response); + return; + } + + // (2) Handle the image request and return the referrer + // result back to the stored queryresult request. + if (request.queryString == "img") { + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + + let referrer = ""; + try { + referrer = request.getHeader("referer"); + } catch (e) { + referrer = ""; + } + // make sure the received image request was upgraded to https, + // otherwise we return not only the referrer but also indicate + // that the request was not upgraded to https. Note, that + // all upgrades happen in the browser before any non-secure + // request hits the wire. + referrer += (request.scheme == "https") ? + "" : " but request is not https"; + + getObjectState("queryResult", function(queryResponse) { + if (!queryResponse) { + return; + } + queryResponse.write(referrer); + queryResponse.finish(); + }); + return; + } + + // we should not get here ever, but just in case return + // something unexpected. + response.write("doh!"); +} diff --git a/dom/security/test/csp/file_upgrade_insecure_reporting.html b/dom/security/test/csp/file_upgrade_insecure_reporting.html new file mode 100644 index 000000000..c78e9a784 --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_reporting.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title> +</head> +<body> + + <!-- upgrade img from http:// to https:// --> + <img id="testimage" src="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_reporting_server.sjs?img"></img> + + <script type="application/javascript"> + var myImg = document.getElementById("testimage"); + myImg.onload = function(e) { + window.parent.postMessage({result: "img-ok"}, "*"); + }; + myImg.onerror = function(e) { + window.parent.postMessage({result: "img-error"}, "*"); + }; + </script> + +</body> +</html> diff --git a/dom/security/test/csp/file_upgrade_insecure_reporting_server.sjs b/dom/security/test/csp/file_upgrade_insecure_reporting_server.sjs new file mode 100644 index 000000000..b9940a7fd --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_reporting_server.sjs @@ -0,0 +1,80 @@ +// Custom *.sjs specifically for the needs of Bug +// Bug 1139297 - Implement CSP upgrade-insecure-requests directive + +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +// small red image +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="); + +const REPORT_URI = "https://example.com/tests/dom/security/test/csp/file_upgrade_insecure_reporting_server.sjs?report"; +const POLICY = "upgrade-insecure-requests; default-src https: 'unsafe-inline'"; +const POLICY_RO = "default-src https: 'unsafe-inline'; report-uri " + REPORT_URI; + +function loadHTMLFromFile(path) { + // Load the HTML to return in the response from file. + // Since it's relative to the cwd of the test runner, we start there and + // append to get to the actual path of the file. + var testHTMLFile = + Components.classes["@mozilla.org/file/directory_service;1"]. + getService(Components.interfaces.nsIProperties). + get("CurWorkD", Components.interfaces.nsILocalFile); + var dirs = path.split("/"); + for (var i = 0; i < dirs.length; i++) { + testHTMLFile.append(dirs[i]); + } + var testHTMLFileStream = + Components.classes["@mozilla.org/network/file-input-stream;1"]. + createInstance(Components.interfaces.nsIFileInputStream); + testHTMLFileStream.init(testHTMLFile, -1, 0, 0); + var testHTML = NetUtil.readInputStreamToString(testHTMLFileStream, testHTMLFileStream.available()); + return testHTML; +} + + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // (1) Store the query that will report back whether the violation report was received + if (request.queryString == "queryresult") { + response.processAsync(); + setObjectState("queryResult", response); + return; + } + + // (2) We load a page using a CSP and a report only CSP + if (request.queryString == "toplevel") { + response.setHeader("Content-Security-Policy", POLICY, false); + response.setHeader("Content-Security-Policy-Report-Only", POLICY_RO, false); + response.setHeader("Content-Type", "text/html", false); + response.write(loadHTMLFromFile("tests/dom/security/test/csp/file_upgrade_insecure_reporting.html")); + return; + } + + // (3) Return the image back to the client + if (request.queryString == "img") { + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + return; + } + + // (4) Finally we receive the report, let's return the request from (1) + // signaling that we received the report correctly + if (request.queryString == "report") { + getObjectState("queryResult", function(queryResponse) { + if (!queryResponse) { + return; + } + queryResponse.write("report-ok"); + queryResponse.finish(); + }); + return; + } + + // we should never get here, but just in case ... + response.setHeader("Content-Type", "text/plain"); + response.write("doh!"); +} diff --git a/dom/security/test/csp/file_upgrade_insecure_server.sjs b/dom/security/test/csp/file_upgrade_insecure_server.sjs new file mode 100644 index 000000000..e27b97830 --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_server.sjs @@ -0,0 +1,102 @@ +// Custom *.sjs file specifically for the needs of Bug: +// Bug 1139297 - Implement CSP upgrade-insecure-requests directive + +const TOTAL_EXPECTED_REQUESTS = 11; + +const IFRAME_CONTENT = + "<!DOCTYPE HTML>" + + "<html>" + + "<head><meta charset='utf-8'>" + + "<title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title>" + + "</head>" + + "<body>" + + "<img src='http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?nested-img'></img>" + + "</body>" + + "</html>"; + +const expectedQueries = [ "script", "style", "img", "iframe", "form", "xhr", + "media", "object", "font", "img-redir", "nested-img"]; + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + var queryString = request.queryString; + + // initialize server variables and save the object state + // of the initial request, which returns async once the + // server has processed all requests. + if (queryString == "queryresult") { + setState("totaltests", TOTAL_EXPECTED_REQUESTS.toString()); + setState("receivedQueries", ""); + response.processAsync(); + setObjectState("queryResult", response); + return; + } + + // handle img redirect (https->http) + if (queryString == "redirect-image") { + var newLocation = + "http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_server.sjs?img-redir"; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", newLocation, false); + return; + } + + // just in case error handling for unexpected queries + if (expectedQueries.indexOf(queryString) == -1) { + response.write("doh!"); + return; + } + + // make sure all the requested queries are indeed https + queryString += (request.scheme == "https") ? "-ok" : "-error"; + + var receivedQueries = getState("receivedQueries"); + + // images, scripts, etc. get queried twice, do not + // confuse the server by storing the preload as + // well as the actual load. If either the preload + // or the actual load is not https, then we would + // append "-error" in the array and the test would + // fail at the end. + if (receivedQueries.includes(queryString)) { + return; + } + + // append the result to the total query string array + if (receivedQueries != "") { + receivedQueries += ","; + } + receivedQueries += queryString; + setState("receivedQueries", receivedQueries); + + // keep track of how many more requests the server + // is expecting + var totaltests = parseInt(getState("totaltests")); + totaltests -= 1; + setState("totaltests", totaltests.toString()); + + // return content (img) for the nested iframe to test + // that subresource requests within nested contexts + // get upgraded as well. We also have to return + // the iframe context in case of an error so we + // can test both, using upgrade-insecure as well + // as the base case of not using upgrade-insecure. + if ((queryString == "iframe-ok") || (queryString == "iframe-error")) { + response.write(IFRAME_CONTENT); + } + + // if we have received all the requests, we return + // the result back. + if (totaltests == 0) { + getObjectState("queryResult", function(queryResponse) { + if (!queryResponse) { + return; + } + var receivedQueries = getState("receivedQueries"); + queryResponse.write(receivedQueries); + queryResponse.finish(); + }); + } +} diff --git a/dom/security/test/csp/file_upgrade_insecure_wsh.py b/dom/security/test/csp/file_upgrade_insecure_wsh.py new file mode 100644 index 000000000..0ae161bff --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_wsh.py @@ -0,0 +1,7 @@ +from mod_pywebsocket import msgutil + +def web_socket_do_extra_handshake(request): + pass + +def web_socket_transfer_data(request): + pass diff --git a/dom/security/test/csp/file_web_manifest.html b/dom/security/test/csp/file_web_manifest.html new file mode 100644 index 000000000..0f6a67460 --- /dev/null +++ b/dom/security/test/csp/file_web_manifest.html @@ -0,0 +1,6 @@ +<!doctype html> +<meta charset=utf-8> +<head> +<link rel="manifest" href="file_web_manifest.json"> +</head> +<h1>Support Page for Web Manifest Tests</h1>
\ No newline at end of file diff --git a/dom/security/test/csp/file_web_manifest.json b/dom/security/test/csp/file_web_manifest.json new file mode 100644 index 000000000..917ab73ef --- /dev/null +++ b/dom/security/test/csp/file_web_manifest.json @@ -0,0 +1 @@ +{"name": "loaded"}
\ No newline at end of file diff --git a/dom/security/test/csp/file_web_manifest.json^headers^ b/dom/security/test/csp/file_web_manifest.json^headers^ new file mode 100644 index 000000000..e0e00c4be --- /dev/null +++ b/dom/security/test/csp/file_web_manifest.json^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: http://example.org
\ No newline at end of file diff --git a/dom/security/test/csp/file_web_manifest_https.html b/dom/security/test/csp/file_web_manifest_https.html new file mode 100644 index 000000000..b0ff9ef85 --- /dev/null +++ b/dom/security/test/csp/file_web_manifest_https.html @@ -0,0 +1,4 @@ +<!doctype html> +<meta charset=utf-8> +<link rel="manifest" href="https://example.com:443/tests/dom/security/test/csp/file_web_manifest_https.json"> +<h1>Support Page for Web Manifest Tests</h1>
\ No newline at end of file diff --git a/dom/security/test/csp/file_web_manifest_https.json b/dom/security/test/csp/file_web_manifest_https.json new file mode 100644 index 000000000..917ab73ef --- /dev/null +++ b/dom/security/test/csp/file_web_manifest_https.json @@ -0,0 +1 @@ +{"name": "loaded"}
\ No newline at end of file diff --git a/dom/security/test/csp/file_web_manifest_mixed_content.html b/dom/security/test/csp/file_web_manifest_mixed_content.html new file mode 100644 index 000000000..55f17c0f9 --- /dev/null +++ b/dom/security/test/csp/file_web_manifest_mixed_content.html @@ -0,0 +1,9 @@ +<!doctype html> +<meta charset=utf-8> +<head> +<link + rel="manifest" + href="http://example.org/tests/dom/security/test/csp/file_testserver.sjs?file=/test/dom/security/test/csp/file_web_manifest.json&cors=*"> +</head> +<h1>Support Page for Web Manifest Tests</h1> +<p>Used to try to load a resource over an insecure connection to trigger mixed content blocking.</p>
\ No newline at end of file diff --git a/dom/security/test/csp/file_web_manifest_remote.html b/dom/security/test/csp/file_web_manifest_remote.html new file mode 100644 index 000000000..7ecf8eec4 --- /dev/null +++ b/dom/security/test/csp/file_web_manifest_remote.html @@ -0,0 +1,8 @@ +<!doctype html> +<meta charset=utf-8> +<link rel="manifest" + crossorigin + href="//mochi.test:8888/tests/dom/security/test/csp/file_testserver.sjs?file=/tests/dom/security/test/csp/file_web_manifest.json&cors=*"> + +<h1>Support Page for Web Manifest Tests</h1> +<p>Loads a manifest from mochi.test:8888 with CORS set to "*".</p>
\ No newline at end of file diff --git a/dom/security/test/csp/mochitest.ini b/dom/security/test/csp/mochitest.ini new file mode 100644 index 000000000..8add999c3 --- /dev/null +++ b/dom/security/test/csp/mochitest.ini @@ -0,0 +1,300 @@ +[DEFAULT] +support-files = + file_base_uri_server.sjs + file_blob_data_schemes.html + file_connect-src.html + file_connect-src-fetch.html + file_CSP.css + file_CSP.sjs + file_allow_https_schemes.html + file_bug663567.xsl + file_bug663567_allows.xml + file_bug663567_allows.xml^headers^ + file_bug663567_blocks.xml + file_bug663567_blocks.xml^headers^ + file_bug802872.html + file_bug802872.html^headers^ + file_bug802872.js + file_bug802872.sjs + file_bug885433_allows.html + file_bug885433_allows.html^headers^ + file_bug885433_blocks.html + file_bug885433_blocks.html^headers^ + file_bug888172.html + file_bug888172.sjs + file_evalscript_main.js + file_evalscript_main_allowed.js + file_evalscript_main.html + file_evalscript_main.html^headers^ + file_evalscript_main_allowed.html + file_evalscript_main_allowed.html^headers^ + file_frameancestors_main.html + file_frameancestors_main.js + file_frameancestors.sjs + file_inlinescript.html + file_inlinestyle_main.html + file_inlinestyle_main.html^headers^ + file_inlinestyle_main_allowed.html + file_inlinestyle_main_allowed.html^headers^ + file_invalid_source_expression.html + file_main.html + file_main.html^headers^ + file_main.js + file_main_worker.js + file_main_worker.js^headers^ + file_child_worker.js + file_child_worker.js^headers^ + file_web_manifest.html + file_web_manifest_remote.html + file_web_manifest_https.html + file_web_manifest.json + file_web_manifest.json^headers^ + file_web_manifest_https.json + file_web_manifest_mixed_content.html + file_bug836922_npolicies.html + file_bug836922_npolicies.html^headers^ + file_bug836922_npolicies_ro_violation.sjs + file_bug836922_npolicies_violation.sjs + file_bug886164.html + file_bug886164.html^headers^ + file_bug886164_2.html + file_bug886164_2.html^headers^ + file_bug886164_3.html + file_bug886164_3.html^headers^ + file_bug886164_4.html + file_bug886164_4.html^headers^ + file_bug886164_5.html + file_bug886164_5.html^headers^ + file_bug886164_6.html + file_bug886164_6.html^headers^ + file_redirects_main.html + file_redirects_page.sjs + file_redirects_resource.sjs + file_bug910139.sjs + file_bug910139.xml + file_bug910139.xsl + file_bug909029_star.html + file_bug909029_star.html^headers^ + file_bug909029_none.html + file_bug909029_none.html^headers^ + file_bug1229639.html + file_bug1229639.html^headers^ + file_bug1312272.html + file_bug1312272.js + file_bug1312272.html^headers^ + file_policyuri_regression_from_multipolicy.html + file_policyuri_regression_from_multipolicy.html^headers^ + file_policyuri_regression_from_multipolicy_policy + file_shouldprocess.html + file_nonce_source.html + file_nonce_source.html^headers^ + file_bug941404.html + file_bug941404_xhr.html + file_bug941404_xhr.html^headers^ + file_hash_source.html + file_dual_header_testserver.sjs + file_hash_source.html^headers^ + file_scheme_relative_sources.js + file_scheme_relative_sources.sjs + file_ignore_unsafe_inline.html + file_ignore_unsafe_inline_multiple_policies_server.sjs + file_self_none_as_hostname_confusion.html + file_self_none_as_hostname_confusion.html^headers^ + file_path_matching.html + file_path_matching_incl_query.html + file_path_matching.js + file_path_matching_redirect.html + file_path_matching_redirect_server.sjs + file_testserver.sjs + file_report_uri_missing_in_report_only_header.html + file_report_uri_missing_in_report_only_header.html^headers^ + file_report.html + file_report_chromescript.js + file_redirect_content.sjs + file_redirect_report.sjs + file_subframe_run_js_if_allowed.html + file_subframe_run_js_if_allowed.html^headers^ + file_leading_wildcard.html + file_multi_policy_injection_bypass.html + file_multi_policy_injection_bypass.html^headers^ + file_multi_policy_injection_bypass_2.html + file_multi_policy_injection_bypass_2.html^headers^ + file_null_baseuri.html + file_form-action.html + file_referrerdirective.html + referrerdirective.sjs + file_upgrade_insecure.html + file_upgrade_insecure_meta.html + file_upgrade_insecure_server.sjs + file_upgrade_insecure_wsh.py + file_upgrade_insecure_reporting.html + file_upgrade_insecure_reporting_server.sjs + file_upgrade_insecure_referrer.sjs + file_upgrade_insecure_referrer_server.sjs + file_upgrade_insecure_cors.html + file_upgrade_insecure_cors_server.sjs + file_report_for_import.css + file_report_for_import.html + file_report_for_import_server.sjs + file_service_worker.html + file_service_worker.js + file_child-src_iframe.html + file_child-src_inner_frame.html + file_child-src_worker.html + file_child-src_worker_data.html + file_child-src_worker-redirect.html + file_child-src_worker.js + file_child-src_service_worker.html + file_child-src_service_worker.js + file_child-src_shared_worker.html + file_child-src_shared_worker_data.html + file_child-src_shared_worker-redirect.html + file_child-src_shared_worker.js + file_redirect_worker.sjs + file_meta_element.html + file_meta_header_dual.sjs + file_docwrite_meta.html + file_doccomment_meta.html + file_docwrite_meta.css + file_docwrite_meta.js + file_multipart_testserver.sjs + file_fontloader.sjs + file_fontloader.woff + file_block_all_mcb.sjs + file_block_all_mixed_content_frame_navigation1.html + file_block_all_mixed_content_frame_navigation2.html + file_form_action_server.sjs + !/image/test/mochitest/blue.png + file_meta_whitespace_skipping.html + file_ping.html + test_iframe_sandbox_top_1.html^headers^ + file_iframe_sandbox_document_write.html + file_sandbox_pass.js + file_sandbox_fail.js + file_sandbox_1.html + file_sandbox_2.html + file_sandbox_3.html + file_sandbox_4.html + file_sandbox_5.html + file_sandbox_6.html + file_sandbox_7.html + file_sandbox_8.html + file_sandbox_9.html + file_sandbox_10.html + file_sandbox_11.html + file_sandbox_12.html + file_sandbox_13.html + file_require_sri_meta.sjs + file_require_sri_meta.js + file_sendbeacon.html + file_upgrade_insecure_docwrite_iframe.sjs + file_data-uri_blocked.html + file_data-uri_blocked.html^headers^ + file_strict_dynamic_js_url.html + file_strict_dynamic_script_events.html + file_strict_dynamic_script_events_xbl.html + file_strict_dynamic_script_inline.html + file_strict_dynamic_script_extern.html + file_strict_dynamic.js + file_strict_dynamic_parser_inserted_doc_write.html + file_strict_dynamic_parser_inserted_doc_write_correct_nonce.html + file_strict_dynamic_non_parser_inserted.html + file_strict_dynamic_non_parser_inserted_inline.html + file_strict_dynamic_unsafe_eval.html + file_strict_dynamic_default_src.html + file_strict_dynamic_default_src.js + file_iframe_srcdoc.sjs + file_iframe_sandbox_srcdoc.html + file_iframe_sandbox_srcdoc.html^headers^ + +[test_base-uri.html] +[test_blob_data_schemes.html] +[test_connect-src.html] +[test_CSP.html] +[test_allow_https_schemes.html] +[test_bug663567.html] +[test_bug802872.html] +[test_bug885433.html] +[test_bug888172.html] +[test_evalscript.html] +[test_frameancestors.html] +skip-if = toolkit == 'android' # Times out, not sure why (bug 1008445) +[test_inlinescript.html] +[test_inlinestyle.html] +[test_invalid_source_expression.html] +[test_bug836922_npolicies.html] +[test_bug886164.html] +[test_redirects.html] +[test_bug910139.html] +[test_bug909029.html] +[test_bug1229639.html] +[test_policyuri_regression_from_multipolicy.html] +[test_nonce_source.html] +[test_bug941404.html] +[test_form-action.html] +[test_hash_source.html] +[test_scheme_relative_sources.html] +[test_ignore_unsafe_inline.html] +[test_self_none_as_hostname_confusion.html] +[test_path_matching.html] +[test_path_matching_redirect.html] +[test_report_uri_missing_in_report_only_header.html] +[test_report.html] +[test_301_redirect.html] +[test_302_redirect.html] +[test_303_redirect.html] +[test_307_redirect.html] +[test_subframe_run_js_if_allowed.html] +[test_leading_wildcard.html] +[test_multi_policy_injection_bypass.html] +[test_null_baseuri.html] +[test_referrerdirective.html] +[test_dual_header.html] +[test_upgrade_insecure.html] +# no ssl support as well as websocket tests do not work (see test_websocket.html) +skip-if = toolkit == 'android' || (os != 'linux' && !debug) # Bug 1316305, Bug 1183300 +[test_upgrade_insecure_reporting.html] +skip-if = toolkit == 'android' +[test_upgrade_insecure_referrer.html] +skip-if = toolkit == 'android' +[test_upgrade_insecure_cors.html] +skip-if = toolkit == 'android' +[test_report_for_import.html] +[test_blocked_uri_in_reports.html] +[test_service_worker.html] +[test_child-src_worker.html] +[test_shouldprocess.html] +# Fennec platform does not support Java applet plugin +skip-if = toolkit == 'android' #investigate in bug 1250814 +[test_child-src_worker_data.html] +[test_child-src_worker-redirect.html] +[test_child-src_iframe.html] +[test_meta_element.html] +[test_meta_header_dual.html] +[test_docwrite_meta.html] +[test_multipartchannel.html] +[test_fontloader.html] +[test_block_all_mixed_content.html] +tags = mcb +[test_block_all_mixed_content_frame_navigation.html] +tags = mcb +[test_form_action_blocks_url.html] +[test_meta_whitespace_skipping.html] +[test_iframe_sandbox.html] +[test_iframe_sandbox_top_1.html] +[test_sandbox.html] +[test_ping.html] +[test_require_sri_meta.html] +[test_sendbeacon.html] +[test_upgrade_insecure_docwrite_iframe.html] +[test_bug1242019.html] +[test_bug1312272.html] +[test_strict_dynamic.html] +[test_strict_dynamic_parser_inserted.html] +[test_strict_dynamic_default_src.html] +[test_iframe_sandbox_srcdoc.html] +[test_iframe_srcdoc.html] +[test_sandbox_allow_scripts.html] +support-files = + file_sandbox_allow_scripts.html + file_sandbox_allow_scripts.html^headers^ diff --git a/dom/security/test/csp/referrerdirective.sjs b/dom/security/test/csp/referrerdirective.sjs new file mode 100644 index 000000000..f238ab452 --- /dev/null +++ b/dom/security/test/csp/referrerdirective.sjs @@ -0,0 +1,36 @@ +// Used for bug 965727 to serve up really simple scripts reflecting the +// referrer sent to load this back to the loader. + + +function handleRequest(request, response) { + // skip speculative loads. + + var splits = request.queryString.split('&'); + var params = {}; + splits.forEach(function(v) { + let parts = v.split('='); + params[parts[0]] = unescape(parts[1]); + }); + + var loadType = params['type']; + var referrerLevel = 'error'; + + if (request.hasHeader('Referer')) { + var referrer = request.getHeader('Referer'); + if (referrer.indexOf("file_testserver.sjs") > -1) { + referrerLevel = "full"; + } else { + referrerLevel = "origin"; + } + } else { + referrerLevel = 'none'; + } + + var theScript = 'window.postResult("' + loadType + '", "' + referrerLevel + '");'; + response.setHeader('Content-Type', 'application/javascript; charset=utf-8', false); + response.setHeader('Cache-Control', 'no-cache', false); + + if (request.method != "OPTIONS") { + response.write(theScript); + } +} diff --git a/dom/security/test/csp/test_301_redirect.html b/dom/security/test/csp/test_301_redirect.html new file mode 100644 index 000000000..8b625a401 --- /dev/null +++ b/dom/security/test/csp/test_301_redirect.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=650386 +Test that CSP violation reports are not sent when a 301 redirect is encountered +--> +<head> + <title>Test for Bug 650386</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650386">Mozilla Bug 650386</a> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe id = "content_iframe"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 650386 **/ + +// This is used to watch the redirect of the report POST get blocked +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} + +examiner.prototype = { + observe: function(subject, topic, data) { + if (topic === "specialpowers-http-notify-request") { + // this is used to fail the test - if we see the POST to the target of the redirect + // we know this is a fail + var uri = data; + if (uri == "http://example.com/some/fake/path") + window.done(false); + } + + if(topic === "csp-on-violate-policy") { + // something was blocked, but we are looking specifically for the redirect being blocked + if (data == "denied redirect while sending violation report") + window.done(true); + } + }, + + // must eventually call this to remove the listener, + // or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} + +window.examiner = new examiner(); + +// result == true if we saw the redirect blocked notify, false if we saw the post +// to the redirect target go out +window.done = function(result) { + ok(result, "a 301 redirect when posting violation report should be blocked"); + + // clean up observers and finish the test + window.examiner.remove(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +// save this for last so that our listeners are registered. +// ... this loads the testbed of good and bad requests. +document.getElementById('content_iframe').src = 'file_redirect_content.sjs?301'; +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_302_redirect.html b/dom/security/test/csp/test_302_redirect.html new file mode 100644 index 000000000..616ecd9eb --- /dev/null +++ b/dom/security/test/csp/test_302_redirect.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=650386 +Test that CSP violation reports are not sent when a 302 redirect is encountered +--> +<head> + <title>Test for Bug 650386</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650386">Mozilla Bug 650386</a> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe id = "content_iframe"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 650386 **/ + +// This is used to watch the redirect of the report POST get blocked +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} + +examiner.prototype = { + observe: function(subject, topic, data) { + if (topic === "specialpowers-http-notify-request") { + // this is used to fail the test - if we see the POST to the target of the redirect + // we know this is a fail + var uri = data; + if (uri == "http://example.com/some/fake/path") + window.done(false); + } + + if(topic === "csp-on-violate-policy") { + // something was blocked, but we are looking specifically for the redirect being blocked + if (data == "denied redirect while sending violation report") + window.done(true); + } + }, + + // must eventually call this to remove the listener, + // or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} + +window.examiner = new examiner(); + +// result == true if we saw the redirect blocked notify, false if we saw the post +// to the redirect target go out +window.done = function(result) { + ok(result, "a 302 redirect when posting violation report should be blocked"); + + // clean up observers and finish the test + window.examiner.remove(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +// save this for last so that our listeners are registered. +// ... this loads the testbed of good and bad requests. +document.getElementById('content_iframe').src = 'file_redirect_content.sjs?302'; +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_303_redirect.html b/dom/security/test/csp/test_303_redirect.html new file mode 100644 index 000000000..9e59a18f6 --- /dev/null +++ b/dom/security/test/csp/test_303_redirect.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=650386 +Test that CSP violation reports are not sent when a 303 redirect is encountered +--> +<head> + <title>Test for Bug 650386</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650386">Mozilla Bug 650386</a> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe id = "content_iframe"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 650386 **/ + +// This is used to watch the redirect of the report POST get blocked +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} + +examiner.prototype = { + observe: function(subject, topic, data) { + if (topic === "specialpowers-http-notify-request") { + // this is used to fail the test - if we see the POST to the target of the redirect + // we know this is a fail + var uri = data; + if (uri == "http://example.com/some/fake/path") + window.done(false); + } + + if(topic === "csp-on-violate-policy") { + // something was blocked, but we are looking specifically for the redirect being blocked + if (data == "denied redirect while sending violation report") + window.done(true); + } + }, + + // must eventually call this to remove the listener, + // or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} + +window.examiner = new examiner(); + +// result == true if we saw the redirect blocked notify, false if we saw the post +// to the redirect target go out +window.done = function(result) { + ok(result, "a 303 redirect when posting violation report should be blocked"); + + // clean up observers and finish the test + window.examiner.remove(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +// save this for last so that our listeners are registered. +// ... this loads the testbed of good and bad requests. +document.getElementById('content_iframe').src = 'file_redirect_content.sjs?303'; +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_307_redirect.html b/dom/security/test/csp/test_307_redirect.html new file mode 100644 index 000000000..5e30b1b86 --- /dev/null +++ b/dom/security/test/csp/test_307_redirect.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=650386 +Test that CSP violation reports are not sent when a 307 redirect is encountered +--> +<head> + <title>Test for Bug 650386</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650386">Mozilla Bug 650386</a> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe id = "content_iframe"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 650386 **/ + +// This is used to watch the redirect of the report POST get blocked +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} + +examiner.prototype = { + observe: function(subject, topic, data) { + if (topic === "specialpowers-http-notify-request") { + // this is used to fail the test - if we see the POST to the target of the redirect + // we know this is a fail + var uri = data; + if (uri == "http://example.com/some/fake/path") + window.done(false); + } + + if(topic === "csp-on-violate-policy") { + // something was blocked, but we are looking specifically for the redirect being blocked + if (data == "denied redirect while sending violation report") + window.done(true); + } + }, + + // must eventually call this to remove the listener, + // or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} + +window.examiner = new examiner(); + +// result == true if we saw the redirect blocked notify, false if we saw the post +// to the redirect target go out +window.done = function(result) { + ok(result, "a 307 redirect when posting violation report should be blocked"); + + // clean up observers and finish the test + window.examiner.remove(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +// save this for last so that our listeners are registered. +// ... this loads the testbed of good and bad requests. +document.getElementById('content_iframe').src = 'file_redirect_content.sjs?307'; + +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_CSP.html b/dom/security/test/csp/test_CSP.html new file mode 100644 index 000000000..1cde9902d --- /dev/null +++ b/dom/security/test/csp/test_CSP.html @@ -0,0 +1,147 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Content Security Policy Connections</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<iframe style="width:200px;height:200px;" id='cspframe'></iframe> +<script class="testbody" type="text/javascript"> + +// These are test results: -1 means it hasn't run, +// true/false is the pass/fail result. +window.tests = { + img_good: -1, + img_bad: -1, + style_good: -1, + style_bad: -1, + frame_good: -1, + frame_bad: -1, + script_good: -1, + script_bad: -1, + xhr_good: -1, + xhr_bad: -1, + fetch_good: -1, + fetch_bad: -1, + beacon_good: -1, + beacon_bad: -1, + worker_xhr_same_bad: -1, + worker_xhr_cross_good: -1, + worker_fetch_same_bad: -1, + worker_fetch_cross_good: -1, + worker_script_same_good: -1, + worker_script_cross_bad: -1, + worker_inherited_xhr_good: -1, + worker_inherited_xhr_bad: -1, + worker_inherited_fetch_good: -1, + worker_inherited_fetch_bad: -1, + worker_inherited_script_good: -1, + worker_inherited_script_bad: -1, + worker_child_xhr_same_bad: -1, + worker_child_xhr_cross_bad: -1, + worker_child_script_same_bad: -1, + worker_child_script_cross_bad: -1, + worker_child_inherited_parent_xhr_bad: -1, + worker_child_inherited_parent_xhr_good: -1, + worker_child_inherited_parent_script_good: -1, + worker_child_inherited_parent_script_bad: -1, + worker_child_inherited_document_xhr_good: -1, + worker_child_inherited_document_xhr_bad: -1, + worker_child_inherited_document_script_good: -1, + worker_child_inherited_document_script_bad: -1, + media_good: -1, + media_bad: -1, + font_good: -1, + font_bad: -1, + object_good: -1, + object_bad: -1, +}; + +// This is used to watch the blocked data bounce off CSP and allowed data +// get sent out to the wire. +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} +examiner.prototype = { + observe: function(subject, topic, data) { + var testpat = new RegExp("testid=([a-z0-9_]+)"); + + //_good things better be allowed! + //_bad things better be stopped! + + // This is a special observer topic that is proxied from + // http-on-modify-request in the parent process to inform us when a URI is + // loaded + if (topic === "specialpowers-http-notify-request") { + var uri = data; + if (!testpat.test(uri)) return; + var testid = testpat.exec(uri)[1]; + + window.testResult(testid, + /_good/.test(testid), + uri + " allowed by csp"); + } + + if (topic === "csp-on-violate-policy") { + // these were blocked... record that they were blocked + var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec"); + if (!testpat.test(asciiSpec)) return; + var testid = testpat.exec(asciiSpec)[1]; + window.testResult(testid, + /_bad/.test(testid), + asciiSpec + " blocked by \"" + data + "\""); + } + }, + + // must eventually call this to remove the listener, + // or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} + +window.examiner = new examiner(); + +window.testResult = function(testname, result, msg) { + // test already complete.... forget it... remember the first result. + if (window.tests[testname] != -1) + return; + + ok(testname in window.tests, "It's a real test"); + window.tests[testname] = result; + is(result, true, testname + ' test: ' + msg); + + // if any test is incomplete, keep waiting + for (var v in window.tests) + if(tests[v] == -1) + return; + + // ... otherwise, finish + window.examiner.remove(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.pushPrefEnv( + {'set':[// This defaults to 0 ("preload none") on mobile (B2G/Android), which + // blocks loading the resource until the user interacts with a + // corresponding widget, which breaks the media_* tests. We set it + // back to the default used by desktop Firefox to get consistent + // behavior. + ["media.preload.default", 2]]}, + function() { + // save this for last so that our listeners are registered. + // ... this loads the testbed of good and bad requests. + document.getElementById('cspframe').src = 'file_main.html'; + }); +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_allow_https_schemes.html b/dom/security/test/csp/test_allow_https_schemes.html new file mode 100644 index 000000000..713464200 --- /dev/null +++ b/dom/security/test/csp/test_allow_https_schemes.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 826805 - Allow http and https for scheme-less sources</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <p id="display"></p> + <div id="content" style="visibility: hidden"> + <iframe style="width:100%;" id="testframe"></iframe> + </div> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +/* Description of the test: + * We are loading the following url (including a fragment portion): + * https://example.com/tests/dom/security/test/csp/file_path_matching.js#foo + * using different policies that lack specification of a scheme. + * + * Since the file is served over http:, the upgrade to https should be + * permitted by CSP in case no port is specified. + */ + +var policies = [ + ["allowed", "example.com"], + ["allowed", "example.com:443"], + ["allowed", "example.com:80"], + ["allowed", "http://*:80"], + ["allowed", "https://*:443"], + // our testing framework only supports :80 and :443, but + // using :8000 in a policy does the trick for the test. + ["blocked", "example.com:8000"], +] + +var counter = 0; +var policy; + +function loadNextTest() { + if (counter == policies.length) { + SimpleTest.finish(); + } + else { + policy = policies[counter++]; + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape("tests/dom/security/test/csp/file_allow_https_schemes.html"); + // append the CSP that should be used to serve the file + src += "&csp=" + escape("default-src 'none'; script-src " + policy[1]); + + document.getElementById("testframe").addEventListener("load", test, false); + document.getElementById("testframe").src = src; + } +} + +function test() { + try { + document.getElementById("testframe").removeEventListener('load', test, false); + var testframe = document.getElementById("testframe"); + var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML; + is(divcontent, policy[0], "should be " + policy[0] + " in test " + (counter - 1) + "!"); + } + catch (e) { + ok(false, "ERROR: could not access content in test " + (counter - 1) + "!"); + } + loadNextTest(); +} + +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_base-uri.html b/dom/security/test/csp/test_base-uri.html new file mode 100644 index 000000000..7b8dcf7e2 --- /dev/null +++ b/dom/security/test/csp/test_base-uri.html @@ -0,0 +1,124 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1045897 - Test CSP base-uri directive</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <p id="display"></p> + <div id="content" style="visibility: hidden"> + <iframe style="width:100%;" id="testframe"></iframe> + </div> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * We load a page in an iframe (served over http://example.com) that tries to + * modify the 'base' either through setting or also removing the base-uri. We + * load that page using different policies and verify that setting the base-uri + * is correctly blocked by CSP. + */ + +SimpleTest.waitForExplicitFinish(); + +var tests = [ + { csp: "base-uri http://mochi.test;", + base1: "http://mochi.test", + base2: "", + action: "enforce-csp", + result: "http://mochi.test", + desc: "CSP allows base uri" + }, + { csp: "base-uri http://example.com;", + base1: "http://mochi.test", + base2: "", + action: "enforce-csp", + result: "http://example.com", + desc: "CSP blocks base uri" + }, + { csp: "base-uri https:", + base1: "http://mochi.test", + base2: "", + action: "enforce-csp", + result: "http://example.com", + desc: "CSP blocks http base" + }, + { csp: "base-uri 'none'", + base1: "http://mochi.test", + base2: "", + action: "enforce-csp", + result: "http://example.com", + desc: "CSP allows no base modification" + }, + { csp: "", + base1: "http://foo:foo/", + base2: "", + action: "enforce-csp", + result: "http://example.com", + desc: "Invalid base should be ignored" + }, + { csp: "base-uri http://mochi.test", + base1: "http://mochi.test", + base2: "http://test1.example.com", + action: "remove-base1", + result: "http://example.com", + desc: "Removing first base should result in fallback base" + }, + { csp: "", + base1: "http://mochi.test", + base2: "http://test1.example.com", + action: "remove-base1", + result: "http://test1.example.com", + desc: "Removing first base should result in the second base" + }, +]; + +// initializing to -1 so we start at index 0 when we start the test +var counter = -1; + +function finishTest() { + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); +} + +// a postMessage handler that is used by sandboxed iframes without +// 'allow-same-origin' to bubble up results back to this main page. +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + var result = event.data.result; + // we only care about the base uri, so instead of comparing the complete uri + // we just make sure that the base is correct which is sufficient here. + ok(result.startsWith(tests[counter].result), + `${tests[counter].desc}: Expected a base URI that starts + with ${tests[counter].result} but got ${result}`); + loadNextTest(); +} + +function loadNextTest() { + counter++; + if (counter == tests.length) { + finishTest(); + return; + } + var src = "http://example.com/tests/dom/security/test/csp/file_base_uri_server.sjs"; + // append the CSP that should be used to serve the file + // please note that we have to include 'unsafe-inline' to permit sending the postMessage + src += "?csp=" + escape("script-src 'unsafe-inline'; " + tests[counter].csp); + // append potential base tags + src += "&base1=" + escape(tests[counter].base1); + src += "&base2=" + escape(tests[counter].base2); + // append potential action + src += "&action=" + escape(tests[counter].action); + + document.getElementById("testframe").src = src; +} + +// start running the tests +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_blob_data_schemes.html b/dom/security/test/csp/test_blob_data_schemes.html new file mode 100644 index 000000000..37b327b10 --- /dev/null +++ b/dom/security/test/csp/test_blob_data_schemes.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1086999 - Wildcard should not match blob:, data:</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * We load an image using a data: and a blob: scheme and make + * sure a CSP containing a single ASTERISK (*) does not whitelist + * those loads. The single ASTERISK character should not match a + * URI's scheme of a type designating globally unique identifier + * (such as blob:, data:, or filesystem:) + */ + +var tests = [ + { + policy : "default-src 'unsafe-inline' blob: data:", + expected : "allowed", + }, + { + policy : "default-src 'unsafe-inline' *", + expected : "blocked" + } +]; + +var testIndex = 0; +var messageCounter = 0; +var curTest; + +// onError handler is over-reporting, hence we make sure that +// we get an error for both testcases: data and blob before we +// move on to the next test. +var dataRan = false; +var blobRan = false; + +// a postMessage handler to communicate the results back to the parent. +window.addEventListener("message", receiveMessage, false); + +function receiveMessage(event) +{ + is(event.data.result, curTest.expected, event.data.scheme + " should be " + curTest.expected); + + if (event.data.scheme === "data") { + dataRan = true; + } + if (event.data.scheme === "blob") { + blobRan = true; + } + if (dataRan && blobRan) { + loadNextTest(); + } +} + +function loadNextTest() { + if (testIndex === tests.length) { + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); + return; + } + + dataRan = false; + blobRan = false; + + curTest = tests[testIndex++]; + // reset the messageCounter to make sure we receive all the postMessages from the iframe + messageCounter = 0; + + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape("tests/dom/security/test/csp/file_blob_data_schemes.html"); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(curTest.policy); + + document.getElementById("testframe").src = src; +} + +SimpleTest.waitForExplicitFinish(); +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_block_all_mixed_content.html b/dom/security/test/csp/test_block_all_mixed_content.html new file mode 100644 index 000000000..d5c4cda8b --- /dev/null +++ b/dom/security/test/csp/test_block_all_mixed_content.html @@ -0,0 +1,99 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1122236 - CSP: Implement block-all-mixed-content</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the tests: + * Test 1: + * We load mixed display content in a frame using the CSP + * directive 'block-all-mixed-content' and observe that the image is blocked. + * + * Test 2: + * We load mixed display content in a frame using a CSP that allows the load + * and observe that the image is loaded. + * + * Test 3: + * We load mixed display content in a frame not using a CSP at all + * and observe that the image is loaded. + * + * Test 4: + * We load mixed display content in a frame using the CSP + * directive 'block-all-mixed-content' and observe that the image is blocked. + * Please note that Test 3 loads the image we are about to load in Test 4 into + * the img cache. Let's make sure the cached (mixed display content) image is + * not allowed to be loaded. + */ + +const BASE_URI = "https://example.com/tests/dom/security/test/csp/"; + +const tests = [ + { // Test 1 + query: "csp-block", + expected: "img-blocked", + description: "(csp-block) block-all-mixed content should block mixed display content" + }, + { // Test 2 + query: "csp-allow", + expected: "img-loaded", + description: "(csp-allow) mixed display content should be loaded" + }, + { // Test 3 + query: "no-csp", + expected: "img-loaded", + description: "(no-csp) mixed display content should be loaded" + }, + { // Test 4 + query: "csp-block", + expected: "img-blocked", + description: "(csp-block) block-all-mixed content should block insecure cache loads" + }, + { // Test 5 + query: "cspro-block", + expected: "img-loaded", + description: "(cspro-block) block-all-mixed in report only mode should not block" + }, +]; + +var curTest; +var counter = -1; + +function checkResults(result) { + is(result, curTest.expected, curTest.description); + loadNextTest(); +} + +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + checkResults(event.data.result); +} + +function loadNextTest() { + counter++; + if (counter == tests.length) { + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); + return; + } + curTest = tests[counter]; + testframe.src = BASE_URI + "file_block_all_mcb.sjs?" + curTest.query; +} + +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.pushPrefEnv( + { 'set': [["security.mixed_content.block_display_content", false]] }, + function() { loadNextTest(); } +); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_block_all_mixed_content_frame_navigation.html b/dom/security/test/csp/test_block_all_mixed_content_frame_navigation.html new file mode 100644 index 000000000..c9d671fd7 --- /dev/null +++ b/dom/security/test/csp/test_block_all_mixed_content_frame_navigation.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1122236 - CSP: Implement block-all-mixed-content</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * + * http://a.com embeds https://b.com. + * https://b.com has a CSP using 'block-all-mixed-content'. + * | site | http://a.com + * | embeds | https://b.com (uses block-all-mixed-content) + * + * The user navigates the embedded frame from + * https://b.com -> http://c.com. + * The test makes sure that such a navigation is not blocked + * by block-all-mixed-content. + */ + +function checkResults(result) { + is(result, "frame-navigated", "frame should be allowed to be navigated"); + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); +} + +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + checkResults(event.data.result); +} + +SimpleTest.waitForExplicitFinish(); +// http://a.com loads https://b.com +document.getElementById("testframe").src = + "https://example.com/tests/dom/security/test/csp/file_block_all_mixed_content_frame_navigation1.html"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_blocked_uri_in_reports.html b/dom/security/test/csp/test_blocked_uri_in_reports.html new file mode 100644 index 000000000..f68d8c03f --- /dev/null +++ b/dom/security/test/csp/test_blocked_uri_in_reports.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1069762 - Check blocked-uri in csp-reports after redirect</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<iframe style="width:200px;height:200px;" id='cspframe'></iframe> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +/* Description of the test: + * We try to load a script from: + * http://example.com/tests/dom/security/test/csp/file_path_matching_redirect_server.sjs + * which gets redirected to: + * http://test1.example.com/tests/dom/security//test/csp/file_path_matching.js + * + * The blocked-uri in the csp-report should be: + * test1.example.com + * instead of: + * http://test1.example.com/tests/com/security/test/csp/file_path_matching.js + * + * see also: http://www.w3.org/TR/CSP/#violation-reports + * + * Note, that we reuse the test-setup from + * test_path_matching_redirect.html + */ + +const reportURI = "http://mochi.test:8888/foo.sjs"; +const policy = "script-src http://example.com; report-uri " + reportURI; +const testfile = "tests/dom/security/test/csp/file_path_matching_redirect.html"; + +var chromeScriptUrl = SimpleTest.getTestFileURL("file_report_chromescript.js"); +var script = SpecialPowers.loadChromeScript(chromeScriptUrl); + +script.addMessageListener('opening-request-completed', function ml(msg) { + if (msg.error) { + ok(false, "Could not query report (exception: " + msg.error + ")"); + } else { + try { + var reportObj = JSON.parse(msg.report); + } catch (e) { + ok(false, "Could not parse JSON (exception: " + e + ")"); + } + try { + var cspReport = reportObj["csp-report"]; + // blocked-uri should only be the asciiHost instead of: + // http://test1.example.com/tests/dom/security/test/csp/file_path_matching.js + is(cspReport["blocked-uri"], "http://test1.example.com", "Incorrect blocked-uri"); + } catch (e) { + ok(false, "Could not query report (exception: " + e + ")"); + } + } + + script.removeMessageListener('opening-request-completed', ml); + SimpleTest.finish(); +}); + +SimpleTest.waitForExplicitFinish(); + +function runTest() { + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape(testfile); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(policy); + + document.getElementById("cspframe").src = src; +} + +runTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_bug1229639.html b/dom/security/test/csp/test_bug1229639.html new file mode 100644 index 000000000..cd322d36d --- /dev/null +++ b/dom/security/test/csp/test_bug1229639.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1229639 - Percent encoded CSP path matching.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> + +<iframe style="width:200px;height:200px;" id='cspframe'></iframe> +<script class="testbody" type="text/javascript"> + +// This is used to watch the blocked data bounce off CSP and allowed data +// get sent out to the wire. +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} + +examiner.prototype = { + observe: function(subject, topic, data) { + if (data === 'http://mochi.test:8888/tests/dom/security/test/csp/%24.js') { + is(topic, "specialpowers-http-notify-request"); + this.remove(); + SimpleTest.finish(); + } + }, + + // must eventually call this to remove the listener, + // or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} + +window.examiner = new examiner(); + +SimpleTest.waitForExplicitFinish(); + +// save this for last so that our listeners are registered. +// ... this loads the testbed of good and bad requests. +document.getElementById('cspframe').src = 'file_bug1229639.html'; + +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_bug1242019.html b/dom/security/test/csp/test_bug1242019.html new file mode 100644 index 000000000..d57fa02bf --- /dev/null +++ b/dom/security/test/csp/test_bug1242019.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1242019 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1242019</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1242019">Mozilla Bug 1242019</a> +<p id="display"></p> + +<iframe id="cspframe"></iframe> + +<pre id="test"> + +<script class="testbody" type="text/javascript"> +function cleanup() { + SpecialPowers.postConsoleSentinel(); + SimpleTest.finish(); +}; + +var expectedURI = "data:image/png;base64,iVBORw0KGgoAAAANSU" + +SpecialPowers.registerConsoleListener(function ConsoleMsgListener(aMsg) { + // look for the message with data uri and see the data uri is truncated to 40 chars + data_start = aMsg.message.indexOf(expectedURI) + if (data_start > -1) { + data_uri = ""; + data_uri = aMsg.message.substr(data_start); + // this will either match the elipsis after the URI or the . at the end of the message + data_uri = data_uri.substr(0, data_uri.indexOf(".")); + if (data_uri == "") { + return; + } + + ok(data_uri.length == 40, "Data URI only shows 40 characters in the console"); + SimpleTest.executeSoon(cleanup); + } +}); + +// set up and start testing +SimpleTest.waitForExplicitFinish(); +document.getElementById('cspframe').src = 'file_data-uri_blocked.html'; +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_bug1312272.html b/dom/security/test/csp/test_bug1312272.html new file mode 100644 index 000000000..2cbebb844 --- /dev/null +++ b/dom/security/test/csp/test_bug1312272.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + + <title>Test for bug 1312272</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe id="cspframe" style="width:100%"></iframe> + +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +function handler(evt) { + console.log(evt); + if (evt.data === "finish") { + ok(true, 'Other events continue to work fine.') + SimpleTest.finish(); + //removeEventListener('message', handler); + } else { + ok(false, "Should not get any other message") + } +} +var cspframe = document.getElementById("cspframe"); +cspframe.src = "file_bug1312272.html"; +addEventListener("message", handler); +console.log("assignign frame"); +</script> + +</body> +</html> diff --git a/dom/security/test/csp/test_bug663567.html b/dom/security/test/csp/test_bug663567.html new file mode 100644 index 000000000..293aa2914 --- /dev/null +++ b/dom/security/test/csp/test_bug663567.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test if XSLT stylesheet is subject to document's CSP</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <iframe style="width:100%;" id='xsltframe'></iframe> + <iframe style="width:100%;" id='xsltframe2'></iframe> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +// define the expected output of this test +var header = "this xml file should be formatted using an xsl file(lower iframe should contain xml dump)!"; + +var finishedTests = 0; +var numberOfTests = 2; + +var checkExplicitFinish = function() { + finishedTests++; + if (finishedTests == numberOfTests) { + SimpleTest.finish(); + } +} + +function checkAllowed () { + /* The policy for this test is: + * Content-Security-Policy: default-src 'self' + * + * we load the xsl file using: + * <?xml-stylesheet type="text/xsl" href="file_bug663467.xsl"?> + */ + try { + var cspframe = document.getElementById('xsltframe'); + var xsltAllowedHeader = cspframe.contentWindow.document.getElementById('xsltheader').innerHTML; + is(xsltAllowedHeader, header, "XSLT loaded from 'self' should be allowed!"); + } + catch (e) { + ok(false, "Error: could not access content in xsltframe!") + } + checkExplicitFinish(); +} + +function checkBlocked () { + /* The policy for this test is: + * Content-Security-Policy: default-src *.example.com + * + * we load the xsl file using: + * <?xml-stylesheet type="text/xsl" href="file_bug663467.xsl"?> + */ + try { + var cspframe = document.getElementById('xsltframe2'); + var xsltBlockedHeader = cspframe.contentWindow.document.getElementById('xsltheader'); + is(xsltBlockedHeader, null, "XSLT loaded from different host should be blocked!"); + } + catch (e) { + ok(false, "Error: could not access content in xsltframe2!") + } + checkExplicitFinish(); +} + +document.getElementById('xsltframe').addEventListener('load', checkAllowed, false); +document.getElementById('xsltframe').src = 'file_bug663567_allows.xml'; + +document.getElementById('xsltframe2').addEventListener('load', checkBlocked, false); +document.getElementById('xsltframe2').src = 'file_bug663567_blocks.xml'; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_bug802872.html b/dom/security/test/csp/test_bug802872.html new file mode 100644 index 000000000..70584c14f --- /dev/null +++ b/dom/security/test/csp/test_bug802872.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 802872</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <iframe style="width:100%;" id='eventframe'></iframe> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var finishedTests = 0; +var numberOfTests = 2; + +var checkExplicitFinish = function () { + finishedTests++; + if (finishedTests == numberOfTests) { + SimpleTest.finish(); + } +} + +// add event listeners for CSP-permitted EventSrc callbacks +addEventListener('allowedEventSrcCallbackOK', function (e) { + ok(true, "OK: CSP allows EventSource for whitelisted domain!"); + checkExplicitFinish(); +}, false); +addEventListener('allowedEventSrcCallbackFailed', function (e) { + ok(false, "Error: CSP blocks EventSource for whitelisted domain!"); + checkExplicitFinish(); +}, false); + +// add event listeners for CSP-blocked EventSrc callbacks +addEventListener('blockedEventSrcCallbackOK', function (e) { + ok(false, "Error: CSP allows EventSource to not whitelisted domain!"); + checkExplicitFinish(); +}, false); +addEventListener('blockedEventSrcCallbackFailed', function (e) { + ok(true, "OK: CSP blocks EventSource for not whitelisted domain!"); + checkExplicitFinish(); +}, false); + +// load it +document.getElementById('eventframe').src = 'file_bug802872.html'; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_bug836922_npolicies.html b/dom/security/test/csp/test_bug836922_npolicies.html new file mode 100644 index 000000000..8d0390eed --- /dev/null +++ b/dom/security/test/csp/test_bug836922_npolicies.html @@ -0,0 +1,240 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Content Security Policy multiple policy support (regular and Report-Only mode)</title> + <script type="text/javascript" src="/MochiKit/packed.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<iframe style="width:200px;height:200px;" id='cspframe'></iframe> +<script class="testbody" type="text/javascript"> + +var path = "/tests/dom/security/test/csp/"; + +// These are test results: verified indicates whether or not the test has run. +// true/false is the pass/fail result. +window.loads = { + css_self: {expected: true, verified: false}, + img_self: {expected: false, verified: false}, + script_self: {expected: true, verified: false}, +}; + +window.violation_reports = { + css_self: + {expected: 0, expected_ro: 0}, /* totally fine */ + img_self: + {expected: 1, expected_ro: 0}, /* violates enforced CSP */ + script_self: + {expected: 0, expected_ro: 1}, /* violates report-only */ +}; + +// This is used to watch the blocked data bounce off CSP and allowed data +// get sent out to the wire. This also watches for violation reports to go out. +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} +examiner.prototype = { + observe: function(subject, topic, data) { + var testpat = new RegExp("testid=([a-z0-9_]+)"); + + if (topic === "specialpowers-http-notify-request") { + var uri = data; + if (!testpat.test(uri)) return; + var testid = testpat.exec(uri)[1]; + + // violation reports don't come through here, but the requested resources do + // if the test has already finished, move on. Some things throw multiple + // requests (preloads and such) + try { + if (window.loads[testid].verified) return; + } catch(e) { return; } + + // these are requests that were allowed by CSP + var testid = testpat.exec(uri)[1]; + window.testResult(testid, 'allowed', uri + " allowed by csp"); + } + + if(topic === "csp-on-violate-policy") { + // if the violated policy was report-only, the resource will still be + // loaded even if this topic is notified. + var asciiSpec = SpecialPowers.getPrivilegedProps( + SpecialPowers.do_QueryInterface(subject, "nsIURI"), + "asciiSpec"); + if (!testpat.test(asciiSpec)) return; + var testid = testpat.exec(asciiSpec)[1]; + + // if the test has already finished, move on. + try { + if (window.loads[testid].verified) return; + } catch(e) { return; } + + // record the ones that were supposed to be blocked, but don't use this + // as an indicator for tests that are not blocked but do generate reports. + // We skip recording the result if the load is expected since a + // report-only policy will generate a request *and* a violation note. + if (!window.loads[testid].expected) { + window.testResult(testid, + 'blocked', + asciiSpec + " blocked by \"" + data + "\""); + } + } + + // if any test is unverified, keep waiting + for (var v in window.loads) { + if(!window.loads[v].verified) { + return; + } + } + + window.bug836922examiner.remove(); + window.resultPoller.pollForFinish(); + }, + + // must eventually call this to remove the listener, + // or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} +window.bug836922examiner = new examiner(); + + +// Poll for results and see if enough reports came in. Keep trying +// for a few seconds before failing with lack of reports. +// Have to do this because there's a race between the async reporting +// and this test finishing, and we don't want to win the race. +window.resultPoller = { + + POLL_ATTEMPTS_LEFT: 14, + + pollForFinish: + function() { + var vr = resultPoller.tallyReceivedReports(); + if (resultPoller.verifyReports(vr, resultPoller.POLL_ATTEMPTS_LEFT < 1)) { + // report success condition. + resultPoller.resetReportServer(); + SimpleTest.finish(); + } else { + resultPoller.POLL_ATTEMPTS_LEFT--; + // try again unless we reached the threshold. + setTimeout(resultPoller.pollForFinish, 100); + } + }, + + resetReportServer: + function() { + var xhr = new XMLHttpRequest(); + var xhr_ro = new XMLHttpRequest(); + xhr.open("GET", "file_bug836922_npolicies_violation.sjs?reset", false); + xhr_ro.open("GET", "file_bug836922_npolicies_ro_violation.sjs?reset", false); + xhr.send(null); + xhr_ro.send(null); + }, + + tallyReceivedReports: + function() { + var xhr = new XMLHttpRequest(); + var xhr_ro = new XMLHttpRequest(); + xhr.open("GET", "file_bug836922_npolicies_violation.sjs?results", false); + xhr_ro.open("GET", "file_bug836922_npolicies_ro_violation.sjs?results", false); + xhr.send(null); + xhr_ro.send(null); + + var received = JSON.parse(xhr.responseText); + var received_ro = JSON.parse(xhr_ro.responseText); + + var results = {enforced: {}, reportonly: {}}; + for (var r in window.violation_reports) { + results.enforced[r] = 0; + results.reportonly[r] = 0; + } + + for (var r in received) { + results.enforced[r] += received[r]; + } + for (var r in received_ro) { + results.reportonly[r] += received_ro[r]; + } + + return results; + }, + + verifyReports: + function(receivedCounts, lastAttempt) { + for (var r in window.violation_reports) { + var exp = window.violation_reports[r].expected; + var exp_ro = window.violation_reports[r].expected_ro; + var rec = receivedCounts.enforced[r]; + var rec_ro = receivedCounts.reportonly[r]; + + // if this test breaks, these are helpful dumps: + //dump(">>> Verifying " + r + "\n"); + //dump(" > Expected: " + exp + " / " + exp_ro + " (ro)\n"); + //dump(" > Received: " + rec + " / " + rec_ro + " (ro) \n"); + + // in all cases, we're looking for *at least* the expected number of + // reports of each type (there could be more in some edge cases). + // If there are not enough, we keep waiting and poll the server again + // later. If there are enough, we can successfully finish. + + if (exp == 0) + is(rec, 0, + "Expected zero enforced-policy violation " + + "reports for " + r + ", got " + rec); + else if (lastAttempt) + ok(rec >= exp, + "Received (" + rec + "/" + exp + ") " + + "enforced-policy reports for " + r); + else if (rec < exp) + return false; // continue waiting for more + + if(exp_ro == 0) + is(rec_ro, 0, + "Expected zero report-only-policy violation " + + "reports for " + r + ", got " + rec_ro); + else if (lastAttempt) + ok(rec_ro >= exp_ro, + "Received (" + rec_ro + "/" + exp_ro + ") " + + "report-only-policy reports for " + r); + else if (rec_ro < exp_ro) + return false; // continue waiting for more + } + + // if we complete the loop, we've found all of the violation + // reports we expect. + if (lastAttempt) return true; + + // Repeat successful tests once more to record successes via ok() + return resultPoller.verifyReports(receivedCounts, true); + } +}; + +window.testResult = function(testname, result, msg) { + // otherwise, make sure the allowed ones are expected and blocked ones are not. + if (window.loads[testname].expected) { + is(result, 'allowed', ">> " + msg); + } else { + is(result, 'blocked', ">> " + msg); + } + window.loads[testname].verified = true; +} + + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +// save this for last so that our listeners are registered. +// ... this loads the testbed of good and bad requests. +document.getElementById('cspframe').src = 'http://mochi.test:8888' + path + 'file_bug836922_npolicies.html'; + +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_bug885433.html b/dom/security/test/csp/test_bug885433.html new file mode 100644 index 000000000..22db0ed24 --- /dev/null +++ b/dom/security/test/csp/test_bug885433.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Content Security Policy inline stylesheets stuff</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<iframe style="width:100%;" id='cspframe'></iframe> +<iframe style="width:100%;" id='cspframe2'></iframe> +<script class="testbody" type="text/javascript"> + +////////////////////////////////////////////////////////////////////// +// set up and go +SimpleTest.waitForExplicitFinish(); + +// utilities for check functions +// black means the style wasn't applied, applied styles are green +var green = 'rgb(0, 128, 0)'; +var black = 'rgb(0, 0, 0)'; + +// We test both script and style execution by observing changes in computed styles +function checkAllowed () { + var cspframe = document.getElementById('cspframe'); + var color; + + color = window.getComputedStyle(cspframe.contentDocument.getElementById('unsafe-inline-script-allowed')).color; + ok(color === green, "Inline script should be allowed"); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('unsafe-eval-script-allowed')).color; + ok(color === green, "Eval should be allowed"); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('unsafe-inline-style-allowed')).color; + ok(color === green, "Inline style should be allowed"); + + document.getElementById('cspframe2').src = 'file_bug885433_blocks.html'; + document.getElementById('cspframe2').addEventListener('load', checkBlocked, false); +} + +function checkBlocked () { + var cspframe = document.getElementById('cspframe2'); + var color; + + color = window.getComputedStyle(cspframe.contentDocument.getElementById('unsafe-inline-script-blocked')).color; + ok(color === black, "Inline script should be blocked"); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('unsafe-eval-script-blocked')).color; + ok(color === black, "Eval should be blocked"); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('unsafe-inline-style-blocked')).color; + ok(color === black, "Inline style should be blocked"); + + SimpleTest.finish(); +} + +document.getElementById('cspframe').src = 'file_bug885433_allows.html'; +document.getElementById('cspframe').addEventListener('load', checkAllowed, false); +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_bug886164.html b/dom/security/test/csp/test_bug886164.html new file mode 100644 index 000000000..74fd98458 --- /dev/null +++ b/dom/security/test/csp/test_bug886164.html @@ -0,0 +1,172 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 886164 - Enforce CSP in sandboxed iframe</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<iframe style="width:200px;height:200px;" id='cspframe' sandbox="allow-same-origin"></iframe> +<iframe style="width:200px;height:200px;" id='cspframe2' sandbox></iframe> +<iframe style="width:200px;height:200px;" id='cspframe3' sandbox="allow-same-origin"></iframe> +<iframe style="width:200px;height:200px;" id='cspframe4' sandbox></iframe> +<iframe style="width:200px;height:200px;" id='cspframe5' sandbox="allow-scripts"></iframe> +<iframe style="width:200px;height:200px;" id='cspframe6' sandbox="allow-same-origin allow-scripts"></iframe> +<script class="testbody" type="text/javascript"> + + +var path = "/tests/dom/security/test/csp/"; + +// These are test results: -1 means it hasn't run, +// true/false is the pass/fail result. +window.tests = { + // sandbox allow-same-origin; 'self' + img_good: -1, // same origin + img_bad: -1, //example.com + + // sandbox; 'self' + img2_bad: -1, //example.com + img2a_good: -1, // same origin & is image + + // sandbox allow-same-origin; 'none' + img3_bad: -1, + img3a_bad: -1, + + // sandbox; 'none' + img4_bad: -1, + img4a_bad: -1, + + // sandbox allow-scripts; 'none' 'unsafe-inline' + img5_bad: -1, + img5a_bad: -1, + script5_bad: -1, + script5a_bad: -1, + + // sandbox allow-same-origin allow-scripts; 'self' 'unsafe-inline' + img6_bad: -1, + script6_bad: -1, +}; + +// a postMessage handler that is used by sandboxed iframes without +// 'allow-same-origin' to communicate pass/fail back to this main page. +// it expects to be called with an object like {ok: true/false, desc: +// <description of the test> which it then forwards to ok() +window.addEventListener("message", receiveMessage, false); + +function receiveMessage(event) +{ + ok_wrapper(event.data.ok, event.data.desc); +} + +var cspTestsDone = false; +var iframeSandboxTestsDone = false; + +// iframe related +var completedTests = 0; +var passedTests = 0; + +function ok_wrapper(result, desc) { + ok(result, desc); + + completedTests++; + + if (result) { + passedTests++; + } + + if (completedTests === 5) { + iframeSandboxTestsDone = true; + if (cspTestsDone) { + SimpleTest.finish(); + } + } +} + + +//csp related + +// This is used to watch the blocked data bounce off CSP and allowed data +// get sent out to the wire. +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} +examiner.prototype = { + observe: function(subject, topic, data) { + var testpat = new RegExp("testid=([a-z0-9_]+)"); + + //_good things better be allowed! + //_bad things better be stopped! + + if (topic === "specialpowers-http-notify-request") { + //these things were allowed by CSP + var uri = data; + if (!testpat.test(uri)) return; + var testid = testpat.exec(uri)[1]; + + window.testResult(testid, + /_good/.test(testid), + uri + " allowed by csp"); + } + + if(topic === "csp-on-violate-policy") { + //these were blocked... record that they were blocked + var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec"); + if (!testpat.test(asciiSpec)) return; + var testid = testpat.exec(asciiSpec)[1]; + window.testResult(testid, + /_bad/.test(testid), + asciiSpec + " blocked by \"" + data + "\""); + } + }, + + // must eventually call this to remove the listener, + // or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} + +window.examiner = new examiner(); + +window.testResult = function(testname, result, msg) { + //test already complete.... forget it... remember the first result. + if (window.tests[testname] != -1) + return; + + window.tests[testname] = result; + ok(result, testname + ' test: ' + msg); + + // if any test is incomplete, keep waiting + for (var v in window.tests) + if(tests[v] == -1) + return; + + // ... otherwise, finish + window.examiner.remove(); + cspTestsDone = true; + if (iframeSandboxTestsDone) { + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); + +// save this for last so that our listeners are registered. +// ... this loads the testbed of good and bad requests. +document.getElementById('cspframe').src = 'file_bug886164.html'; +document.getElementById('cspframe2').src = 'file_bug886164_2.html'; +document.getElementById('cspframe3').src = 'file_bug886164_3.html'; +document.getElementById('cspframe4').src = 'file_bug886164_4.html'; +document.getElementById('cspframe5').src = 'file_bug886164_5.html'; +document.getElementById('cspframe6').src = 'file_bug886164_6.html'; + +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_bug888172.html b/dom/security/test/csp/test_bug888172.html new file mode 100644 index 000000000..200e8c942 --- /dev/null +++ b/dom/security/test/csp/test_bug888172.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 888172 - CSP 1.0 does not process 'unsafe-inline' or 'unsafe-eval' for default-src</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<iframe style="width:100%;" id='testframe1'></iframe> +<iframe style="width:100%;" id='testframe2'></iframe> +<iframe style="width:100%;" id='testframe3'></iframe> +<script class="testbody" type="text/javascript"> + +////////////////////////////////////////////////////////////////////// +// set up and go +SimpleTest.waitForExplicitFinish(); + +// utilities for check functions +// black means the style wasn't applied, applied styles are green +var green = 'rgb(0, 128, 0)'; +var black = 'rgb(0, 0, 0)'; + +function getElementColorById(doc, id) { + return window.getComputedStyle(doc.contentDocument.getElementById(id)).color; +} + +// We test both script and style execution by observing changes in computed styles +function checkDefaultSrcOnly() { + var testframe = document.getElementById('testframe1'); + + ok(getElementColorById(testframe, 'unsafe-inline-script') === green, "Inline script should be allowed"); + ok(getElementColorById(testframe, 'unsafe-eval-script') === green, "Eval should be allowed"); + ok(getElementColorById(testframe, 'unsafe-inline-style') === green, "Inline style should be allowed"); + + document.getElementById('testframe2').src = 'file_bug888172.sjs?csp=' + + escape("default-src 'self' 'unsafe-inline' 'unsafe-eval'; script-src 'self'"); + document.getElementById('testframe2').addEventListener('load', checkDefaultSrcWithScriptSrc, false); +} + +function checkDefaultSrcWithScriptSrc() { + var testframe = document.getElementById('testframe2'); + + ok(getElementColorById(testframe, 'unsafe-inline-script') === black, "Inline script should be blocked"); + ok(getElementColorById(testframe, 'unsafe-eval-script') === black, "Eval should be blocked"); + ok(getElementColorById(testframe, 'unsafe-inline-style') === green, "Inline style should be allowed"); + + document.getElementById('testframe3').src = 'file_bug888172.sjs?csp=' + + escape("default-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self'"); + document.getElementById('testframe3').addEventListener('load', checkDefaultSrcWithStyleSrc, false); +} + +function checkDefaultSrcWithStyleSrc() { + var testframe = document.getElementById('testframe3'); + + ok(getElementColorById(testframe, 'unsafe-inline-script') === green, "Inline script should be allowed"); + ok(getElementColorById(testframe, 'unsafe-eval-script') === green, "Eval should be allowed"); + ok(getElementColorById(testframe, 'unsafe-inline-style') === black, "Inline style should be blocked"); + + // last test calls finish + SimpleTest.finish(); +} + +document.getElementById('testframe1').src = 'file_bug888172.sjs?csp=' + + escape("default-src 'self' 'unsafe-inline' 'unsafe-eval'"); +document.getElementById('testframe1').addEventListener('load', checkDefaultSrcOnly, false); +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_bug909029.html b/dom/security/test/csp/test_bug909029.html new file mode 100644 index 000000000..aebfabf48 --- /dev/null +++ b/dom/security/test/csp/test_bug909029.html @@ -0,0 +1,129 @@ +<!doctype html> +<html> + <head> + <title>Bug 909029 - CSP source-lists ignore some source expressions like 'unsafe-inline' when * or 'none' are used (e.g., style-src, script-src)</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <div id=content style="visibility:hidden"> + <iframe id=testframe1></iframe> + <iframe id=testframe2></iframe> + </div> + <script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +window.tests = { + starExternalStylesLoaded: -1, + starExternalImgLoaded: -1, + noneExternalStylesBlocked: -1, + noneExternalImgLoaded: -1, + starInlineStyleAllowed: -1, + starInlineScriptBlocked: -1, + noneInlineStyleAllowed: -1, + noneInlineScriptBlocked: -1 +} + +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} +examiner.prototype = { + observe: function(subject, topic, data) { + var testpat = new RegExp("testid=([a-zA-Z]+)"); + + if (topic === "specialpowers-http-notify-request") { + var uri = data; + if (!testpat.test(uri)) return; + var testid = testpat.exec(uri)[1]; + window.testResult(testid, + /Loaded/.test(testid), + "resource loaded"); + } + + if(topic === "csp-on-violate-policy") { + // these were blocked... record that they were blocked + // try because the subject could be an nsIURI or an nsISupportsCString + try { + var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec"); + if (!testpat.test(asciiSpec)) return; + var testid = testpat.exec(asciiSpec)[1]; + window.testResult(testid, + /Blocked/.test(testid), + "resource blocked by CSP"); + } catch(e) { + // if that fails, the subject is probably a string. Strings are only + // reported for inline and eval violations. Since we are testing those + // via the observed effects of script on CSSOM, we can simply ignore + // these subjects. + } + } + }, + + // must eventually call this to remove the listener, + // or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} + +window.examiner = new examiner(); + +window.testResult = function(testname, result, msg) { + //dump("in testResult: testname = " + testname + "\n"); + + //test already complete.... forget it... remember the first result. + if (window.tests[testname] != -1) + return; + + window.tests[testname] = result; + is(result, true, testname + ' test: ' + msg); + + // if any test is incomplete, keep waiting + for (var v in window.tests) + if(tests[v] == -1) + return; + + // ... otherwise, finish + window.examiner.remove(); + SimpleTest.finish(); +} + +// Helpers for inline script/style checks +var black = 'rgb(0, 0, 0)'; +var green = 'rgb(0, 128, 0)'; +function getElementColorById(doc, id) { + return window.getComputedStyle(doc.contentDocument.getElementById(id)).color; +} + +function checkInlineWithStar() { + var testframe = document.getElementById('testframe1'); + window.testResult("starInlineStyleAllowed", + getElementColorById(testframe, 'inline-style') === green, + "Inline styles should be allowed (style-src 'unsafe-inline' with star)"); + window.testResult("starInlineScriptBlocked", + getElementColorById(testframe, 'inline-script') === black, + "Inline scripts should be blocked (style-src 'unsafe-inline' with star)"); +} + +function checkInlineWithNone() { + // If a directive has 'none' in addition to other sources, 'none' is ignored + // and the other sources are used. 'none' is only a valid source if it is + // used by itself. + var testframe = document.getElementById('testframe2'); + window.testResult("noneInlineStyleAllowed", + getElementColorById(testframe, 'inline-style') === green, + "Inline styles should be allowed (style-src 'unsafe-inline' with none)"); + window.testResult("noneInlineScriptBlocked", + getElementColorById(testframe, 'inline-script') === black, + "Inline scripts should be blocked (style-src 'unsafe-inline' with none)"); +} + +document.getElementById('testframe1').src = 'file_bug909029_star.html'; +document.getElementById('testframe1').addEventListener('load', checkInlineWithStar, false); +document.getElementById('testframe2').src = 'file_bug909029_none.html'; +document.getElementById('testframe2').addEventListener('load', checkInlineWithNone, false); + </script> + </body> +</html> diff --git a/dom/security/test/csp/test_bug910139.html b/dom/security/test/csp/test_bug910139.html new file mode 100644 index 000000000..63a7f77d1 --- /dev/null +++ b/dom/security/test/csp/test_bug910139.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>CSP should block XSLT as script, not as style</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <iframe style="width:100%;" id='xsltframe'></iframe> + <iframe style="width:100%;" id='xsltframe2'></iframe> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +// define the expected output of this test +var header = "this xml file should be formatted using an xsl file(lower iframe should contain xml dump)!"; + +function checkAllowed () { + /* The policy for this test is: + * Content-Security-Policy: default-src 'self'; script-src 'self' + * + * we load the xsl file using: + * <?xml-stylesheet type="text/xsl" href="file_bug910139.xsl"?> + */ + try { + var cspframe = document.getElementById('xsltframe'); + var xsltAllowedHeader = cspframe.contentWindow.document.getElementById('xsltheader').innerHTML; + is(xsltAllowedHeader, header, "XSLT loaded from 'self' should be allowed!"); + } + catch (e) { + ok(false, "Error: could not access content in xsltframe!") + } + + // continue with the next test + document.getElementById('xsltframe2').addEventListener('load', checkBlocked, false); + document.getElementById('xsltframe2').src = 'file_bug910139.sjs'; +} + +function checkBlocked () { + /* The policy for this test is: + * Content-Security-Policy: default-src 'self'; script-src *.example.com + * + * we load the xsl file using: + * <?xml-stylesheet type="text/xsl" href="file_bug910139.xsl"?> + */ + try { + var cspframe = document.getElementById('xsltframe2'); + var xsltBlockedHeader = cspframe.contentWindow.document.getElementById('xsltheader'); + is(xsltBlockedHeader, null, "XSLT loaded from different host should be blocked!"); + } + catch (e) { + ok(false, "Error: could not access content in xsltframe2!") + } + SimpleTest.finish(); +} + +document.getElementById('xsltframe').addEventListener('load', checkAllowed, false); +document.getElementById('xsltframe').src = 'file_bug910139.sjs'; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_bug941404.html b/dom/security/test/csp/test_bug941404.html new file mode 100644 index 000000000..07f45d176 --- /dev/null +++ b/dom/security/test/csp/test_bug941404.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 941404 - Data documents should not set CSP</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + + +</div> + +<iframe style="width:200px;height:200px;" id='cspframe'></iframe> +<script class="testbody" type="text/javascript"> + + +var path = "/tests/dom/security/test/csp/"; + +// These are test results: -1 means it hasn't run, +// true/false is the pass/fail result. +window.tests = { + img_good: -1, + img2_good: -1, +}; + + +//csp related + +// This is used to watch the blocked data bounce off CSP and allowed data +// get sent out to the wire. +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} + +examiner.prototype = { + observe: function(subject, topic, data) { + var testpat = new RegExp("testid=([a-z0-9_]+)"); + + //_good things better be allowed! + //_bad things better be stopped! + + if (topic === "specialpowers-http-notify-request") { + //these things were allowed by CSP + var uri = data; + if (!testpat.test(uri)) return; + var testid = testpat.exec(uri)[1]; + + window.testResult(testid, + /_good/.test(testid), + uri + " allowed by csp"); + } + + if(topic === "csp-on-violate-policy") { + //these were blocked... record that they were blocked + var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec"); + if (!testpat.test(asciiSpec)) return; + var testid = testpat.exec(asciiSpec)[1]; + window.testResult(testid, + /_bad/.test(testid), + asciiSpec + " blocked by \"" + data + "\""); + } + }, + + // must eventually call this to remove the listener, + // or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} + +window.examiner = new examiner(); + +window.testResult = function(testname, result, msg) { + //test already complete.... forget it... remember the first result. + if (window.tests[testname] != -1) + return; + + window.tests[testname] = result; + is(result, true, testname + ' test: ' + msg); + + // if any test is incomplete, keep waiting + for (var v in window.tests) + if(tests[v] == -1) { + console.log(v + " is not complete"); + return; + } + + // ... otherwise, finish + window.examiner.remove(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +// save this for last so that our listeners are registered. +// ... this loads the testbed of good and bad requests. +document.getElementById('cspframe').src = 'file_bug941404.html'; + +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_child-src_iframe.html b/dom/security/test/csp/test_child-src_iframe.html new file mode 100644 index 000000000..b4ba36f89 --- /dev/null +++ b/dom/security/test/csp/test_child-src_iframe.html @@ -0,0 +1,114 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1045891</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <p id="display"></p> + <div id="content" style="visibility: hidden"> + </div> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * We load a page with a given CSP and verify that child frames and workers are correctly + * evaluated through the "child-src" directive. + */ + +SimpleTest.waitForExplicitFinish(); + +var IFRAME_SRC="file_child-src_iframe.html" + +var tests = { + 'same-src': { + id: "same-src", + file: IFRAME_SRC, + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src http://mochi.test:8888" + }, + 'star-src': { + id: "star-src", + file: IFRAME_SRC, + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src *" + }, + 'other-src': { + id: "other-src", + file: IFRAME_SRC, + result : "blocked", + policy : "default-src http://mochi.test:8888; script-src 'unsafe-inline'; child-src http://www.example.com" + }, + 'same-src-by-frame-src': { + id: "same-src-by-frame-src", + file: IFRAME_SRC, + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src 'none'; frame-src http://mochi.test:8888" + }, + 'star-src-by-frame-src': { + id: "star-src-by-frame-src", + file: IFRAME_SRC, + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src 'none'; frame-src *" + }, + 'other-src-by-frame-src': { + id: "other-src-by-frame-src", + file: IFRAME_SRC, + result : "blocked", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src http://mochi.test:8888; frame-src http://www.example.com" + }, + 'none-src-by-frame-src': { + id: "none-src-by-frame-src", + file: "file_child-src_iframe.html", + file: IFRAME_SRC, + result : "blocked", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src http://mochi.test:8888; frame-src 'none'" + } +}; + +finished = {}; + +function checkFinished() { + if (Object.keys(finished).length == Object.keys(tests).length) { + window.removeEventListener('message', recvMessage); + SimpleTest.finish(); + } +} + +function recvMessage(ev) { + is(ev.data.message, tests[ev.data.id].result, "CSP child-src test " + ev.data.id); + finished[ev.data.id] = ev.data.message; + + checkFinished(); +} + +window.addEventListener('message', recvMessage, false); + +function loadNextTest() { + for (item in tests) { + test = tests[item]; + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape("tests/dom/security/test/csp/" + test.file); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(test.policy); + // add our identifier + src += "#" + escape(test.id); + + content = document.getElementById('content'); + testframe = document.createElement("iframe"); + testframe.setAttribute('id', test.id); + content.appendChild(testframe); + testframe.src = src; + } +} + +// start running the tests +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_child-src_worker-redirect.html b/dom/security/test/csp/test_child-src_worker-redirect.html new file mode 100644 index 000000000..dfb99149c --- /dev/null +++ b/dom/security/test/csp/test_child-src_worker-redirect.html @@ -0,0 +1,125 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1045891</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <p id="display"></p> + <div id="content" style="visibility: hidden"> + </div> + + <script class="testbody" type="text/javascript"> + /* + * Description of the test: + * We load a page with a given CSP and verify that child frames and workers are correctly + * evaluated through the "child-src" directive. + */ + + SimpleTest.waitForExplicitFinish(); + + var WORKER_REDIRECT_TEST_FILE = "file_child-src_worker-redirect.html"; + var SHARED_WORKER_REDIRECT_TEST_FILE = "file_child-src_shared_worker-redirect.html"; + + var tests = { + 'same-src-worker_redir-same': { + id: "same-src-worker_redir-same", + file: WORKER_REDIRECT_TEST_FILE, + result : "allowed", + redir: "same", + policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src http://mochi.test:8888" + }, + 'same-src-worker_redir-other': { + id: "same-src-worker_redir-other", + file: WORKER_REDIRECT_TEST_FILE, + result : "blocked", + redir: "other", + policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src http://mochi.test:8888" + }, + 'star-src-worker_redir-same': { + id: "star-src-worker_redir-same", + file: WORKER_REDIRECT_TEST_FILE, + redir: "same", + result : "allowed", + policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src *" + }, + 'other-src-worker_redir-same': { + id: "other-src-worker_redir-same", + file: WORKER_REDIRECT_TEST_FILE, + redir: "same", + result : "blocked", + policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src https://www.example.org" + }, + /* shared workers */ + 'same-src-shared_worker_redir-same': { + id: "same-src-shared_worker_redir-same", + file: SHARED_WORKER_REDIRECT_TEST_FILE, + result : "allowed", + redir: "same", + policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src http://mochi.test:8888" + }, + 'same-src-shared_worker_redir-other': { + id: "same-src-shared_worker_redir-other", + file: SHARED_WORKER_REDIRECT_TEST_FILE, + result : "blocked", + redir: "other", + policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src http://mochi.test:8888" + }, + 'star-src-shared_worker_redir-same': { + id: "star-src-shared_worker_redir-same", + file: SHARED_WORKER_REDIRECT_TEST_FILE, + redir: "same", + result : "allowed", + policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src *" + }, + 'other-src-shared_worker_redir-same': { + id: "other-src-shared_worker_redir-same", + file: SHARED_WORKER_REDIRECT_TEST_FILE, + redir: "same", + result : "blocked", + policy : "default-src 'none'; script-src 'self' 'unsafe-inline'; child-src https://www.example.org" + }, + }; + + finished = {}; + + function recvMessage(ev) { + is(ev.data.message, tests[ev.data.id].result, "CSP child-src worker test " + ev.data.id); + finished[ev.data.id] = ev.data.message; + + if (Object.keys(finished).length == Object.keys(tests).length) { + window.removeEventListener('message', recvMessage); + SimpleTest.finish(); + } + } + + window.addEventListener('message', recvMessage, false); + + function loadNextTest() { + for (item in tests) { + test = tests[item]; + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape("tests/dom/security/test/csp/" + test.file); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(test.policy); + // add whether redirect is to same or different + src += "&redir=" + escape(test.policy); + // add our identifier + src += "#" + escape(test.id); + + content = document.getElementById('content'); + testframe = document.createElement("iframe"); + testframe.setAttribute('id', test.id); + content.appendChild(testframe); + testframe.src = src; + } + } + + // start running the tests + loadNextTest(); + </script> + </body> +</html> diff --git a/dom/security/test/csp/test_child-src_worker.html b/dom/security/test/csp/test_child-src_worker.html new file mode 100644 index 000000000..7dcbd03f6 --- /dev/null +++ b/dom/security/test/csp/test_child-src_worker.html @@ -0,0 +1,148 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1045891</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <p id="display"></p> + <div id="content" style="visibility: hidden"> + </div> + + <script class="testbody" type="text/javascript"> + /* + * Description of the test: + * We load a page with a given CSP and verify that child frames and workers are correctly + * evaluated through the "child-src" directive. + */ + + SimpleTest.waitForExplicitFinish(); + + var WORKER_TEST_FILE = "file_child-src_worker.html"; + var SERVICE_WORKER_TEST_FILE = "file_child-src_service_worker.html"; + var SHARED_WORKER_TEST_FILE = "file_child-src_shared_worker.html"; + + var tests = { + 'same-src-worker': { + id: "same-src-worker", + file: WORKER_TEST_FILE, + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src http://mochi.test:8888" + }, + 'same-src-service_worker': { + id: "same-src-service_worker", + file: SERVICE_WORKER_TEST_FILE, + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src http://mochi.test:8888" + }, + 'same-src-shared_worker': { + id: "same-src-shared_worker", + file: SHARED_WORKER_TEST_FILE, + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src http://mochi.test:8888" + }, + 'star-src-worker': { + id: "star-src-worker", + file: WORKER_TEST_FILE, + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src *" + }, + 'star-src-service_worker': { + id: "star-src-service_worker", + file: SERVICE_WORKER_TEST_FILE, + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src *" + }, + 'star-src-shared_worker': { + id: "star-src-shared_worker", + file: SHARED_WORKER_TEST_FILE, + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src *" + }, + 'other-src-worker': { + id: "other-src-worker", + file: WORKER_TEST_FILE, + result : "blocked", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src https://www.example.org" + }, + 'other-src-service_worker': { + id: "other-src-service_worker", + file: SERVICE_WORKER_TEST_FILE, + result : "blocked", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src https://www.example.org" + }, + 'other-src-shared_worker': { + id: "other-src-shared_worker", + file: SHARED_WORKER_TEST_FILE, + result : "blocked", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src https://www.example.org" + }, + 'script-src-worker': { + id: "script-src-worker", + file: WORKER_TEST_FILE, + result : "blocked", + policy : "default-src 'none'; script-src 'self' 'unsafe-inline'" + }, + 'script-src-service_worker': { + id: "script-src-service_worker", + file: SERVICE_WORKER_TEST_FILE, + result : "blocked", + policy : "default-src 'none'; script-src 'self' 'unsafe-inline'" + }, + 'script-src-self-shared_worker': { + id: "script-src-self-shared_worker", + file: SHARED_WORKER_TEST_FILE, + result : "blocked", + policy : "default-src 'none'; script-src 'self' 'unsafe-inline'" + }, + }; + + finished = {}; + + function recvMessage(ev) { + is(ev.data.message, tests[ev.data.id].result, "CSP child-src worker test " + ev.data.id); + finished[ev.data.id] = ev.data.message; + + if (Object.keys(finished).length == Object.keys(tests).length) { + window.removeEventListener('message', recvMessage); + SimpleTest.finish(); + } + } + + window.addEventListener('message', recvMessage, false); + + function loadNextTest() { + for (item in tests) { + test = tests[item]; + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape("tests/dom/security/test/csp/" + test.file); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(test.policy); + // add our identifier + src += "#" + escape(test.id); + + content = document.getElementById('content'); + testframe = document.createElement("iframe"); + testframe.setAttribute('id', test.id); + content.appendChild(testframe); + testframe.src = src; + } + } + + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true] + ]}, loadNextTest); + }; + + // start running the tests + //loadNextTest(); + </script> + </body> +</html> diff --git a/dom/security/test/csp/test_child-src_worker_data.html b/dom/security/test/csp/test_child-src_worker_data.html new file mode 100644 index 000000000..089d32dbe --- /dev/null +++ b/dom/security/test/csp/test_child-src_worker_data.html @@ -0,0 +1,126 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1045891</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <p id="display"></p> + <div id="content" style="visibility: hidden"> + </div> + + <script class="testbody" type="text/javascript"> + /* + * Description of the test: + * We load a page with a given CSP and verify that child frames and workers are correctly + * evaluated through the "child-src" directive. + */ + + SimpleTest.waitForExplicitFinish(); + + var WORKER_TEST_FILE = "file_child-src_worker_data.html"; + var SHARED_WORKER_TEST_FILE = "file_child-src_shared_worker_data.html"; + + var tests = { + 'same-src-worker-no-data': { + id: "same-src-worker-no-data", + file: WORKER_TEST_FILE, + result : "blocked", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src 'self'" + }, + 'same-src-worker': { + id: "same-src-worker", + file: WORKER_TEST_FILE, + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src 'self' data:" + }, + 'same-src-shared_worker-no-data': { + id: "same-src-shared_worker-no-data", + file: SHARED_WORKER_TEST_FILE, + result : "blocked", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src 'self'" + }, + 'same-src-shared_worker': { + id: "same-src-shared_worker", + file: SHARED_WORKER_TEST_FILE, + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src 'self' data:" + }, + 'star-src-worker': { + id: "star-src-worker", + file: WORKER_TEST_FILE, + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src * data:" + }, + 'star-src-worker-no-data': { + id: "star-src-worker-no-data", + file: WORKER_TEST_FILE, + result : "blocked", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src *" + }, + 'star-src-shared_worker-no-data': { + id: "star-src-shared_worker-no-data", + file: SHARED_WORKER_TEST_FILE, + result : "blocked", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src *" + }, + 'star-src-shared_worker': { + id: "star-src-shared_worker", + file: SHARED_WORKER_TEST_FILE, + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src * data:" + }, + 'other-src-worker-no-data': { + id: "other-src-worker-no-data", + file: WORKER_TEST_FILE, + result : "blocked", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src https://www.example.org" + }, + 'other-src-shared_worker-no-data': { + id: "other-src-shared_worker-no-data", + file: SHARED_WORKER_TEST_FILE, + result : "blocked", + policy : "default-src 'none'; script-src 'unsafe-inline'; child-src https://www.example.org" + }, + }; + + finished = {}; + + function recvMessage(ev) { + is(ev.data.message, tests[ev.data.id].result, "CSP child-src worker test " + ev.data.id); + finished[ev.data.id] = ev.data.message; + + if (Object.keys(finished).length == Object.keys(tests).length) { + window.removeEventListener('message', recvMessage); + SimpleTest.finish(); + } + } + + window.addEventListener('message', recvMessage, false); + + function loadNextTest() { + for (item in tests) { + test = tests[item]; + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape("tests/dom/security/test/csp/" + test.file); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(test.policy); + // add our identifier + src += "#" + escape(test.id); + + content = document.getElementById('content'); + testframe = document.createElement("iframe"); + testframe.setAttribute('id', test.id); + content.appendChild(testframe); + testframe.src = src; + } + } + + // start running the tests + loadNextTest(); + </script> + </body> +</html> diff --git a/dom/security/test/csp/test_connect-src.html b/dom/security/test/csp/test_connect-src.html new file mode 100644 index 000000000..5e42d9838 --- /dev/null +++ b/dom/security/test/csp/test_connect-src.html @@ -0,0 +1,129 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1031530 and Bug 1139667 - Test mapping of XMLHttpRequest and fetch() to connect-src</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <p id="display"></p> + <div id="content" style="visibility: hidden"> + <iframe style="width:100%;" id="testframe"></iframe> + </div> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * We load a page with a given CSP and verify that XMLHttpRequests and fetches are correctly + * evaluated through the "connect-src" directive. All XMLHttpRequests are served + * using http://mochi.test:8888, which allows the requests to succeed for the first + * two policies and to fail for the last policy. Please note that we have to add + * 'unsafe-inline' so we can run the JS test code in file_connect-src.html. + */ + +SimpleTest.waitForExplicitFinish(); + +var tests = [ + { + file: "file_connect-src.html", + result : "allowed", + policy : "default-src 'none' script-src 'unsafe-inline'; connect-src http://mochi.test:8888" + }, + { + file: "file_connect-src.html", + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; connect-src *" + }, + { + file: "file_connect-src.html", + result : "blocked", + policy : "default-src 'none'; script-src 'unsafe-inline'; connect-src http://www.example.com" + }, + { + file: "file_connect-src-fetch.html", + result : "allowed", + policy : "default-src 'none' script-src 'unsafe-inline'; connect-src http://mochi.test:8888" + }, + { + file: "file_connect-src-fetch.html", + result : "allowed", + policy : "default-src 'none'; script-src 'unsafe-inline'; connect-src *" + }, + { + file: "file_connect-src-fetch.html", + result : "blocked", + policy : "default-src 'none'; script-src 'unsafe-inline'; connect-src http://www.example.com" + } +]; + +// initializing to -1 so we start at index 0 when we start the test +var counter = -1; + +function checkResult(aResult) { + is(aResult, tests[counter].result, "should be " + tests[counter].result + " in test " + counter + "!"); + loadNextTest(); +} + +// We use the examiner to identify requests that hit the wire and requests +// that are blocked by CSP and bubble up the result to the including iframe +// document (parent). +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} +examiner.prototype = { + observe: function(subject, topic, data) { + if (topic === "specialpowers-http-notify-request") { + // making sure we do not bubble a result for something other + // then the request in question. + if (!data.includes("file_testserver.sjs?foo")) { + return; + } + checkResult("allowed"); + } + + if (topic === "csp-on-violate-policy") { + // making sure we do not bubble a result for something other + // then the request in question. + var asciiSpec = SpecialPowers.getPrivilegedProps( + SpecialPowers.do_QueryInterface(subject, "nsIURI"), + "asciiSpec"); + + if (!asciiSpec.includes("file_testserver.sjs?foo")) { + return; + } + checkResult("blocked"); + } + }, + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} +window.ConnectSrcExaminer = new examiner(); + +function loadNextTest() { + counter++; + if (counter == tests.length) { + window.ConnectSrcExaminer.remove(); + SimpleTest.finish(); + return; + } + + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape("tests/dom/security/test/csp/" + tests[counter].file); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(tests[counter].policy); + + document.getElementById("testframe").src = src; +} + +// start running the tests +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_docwrite_meta.html b/dom/security/test/csp/test_docwrite_meta.html new file mode 100644 index 000000000..26794199a --- /dev/null +++ b/dom/security/test/csp/test_docwrite_meta.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 663570 - Implement Content Security Policy via meta tag</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<iframe style="width:100%;" id="writemetacspframe"></iframe> +<iframe style="width:100%;" id="commentmetacspframe"></iframe> + + +<script class="testbody" type="text/javascript"> +/* Description of the test: + * We load two frames, where the first frame does doc.write(meta csp) and + * the second does doc.write(comment out meta csp). + * We make sure to reuse/invalidate preloads depending on the policy. + */ + +SimpleTest.waitForExplicitFinish(); + +var writemetacspframe = document.getElementById("writemetacspframe"); +var commentmetacspframe = document.getElementById("commentmetacspframe"); +var seenResults = 0; + +function checkTestsDone() { + seenResults++; + if (seenResults < 2) { + return; + } + SimpleTest.finish(); +} + +// document.write(<meta csp ...>) should block resources from being included in the doc +function checkResultsBlocked() { + writemetacspframe.removeEventListener('load', checkResultsBlocked, false); + + // stylesheet: default background color within FF is transparent + var bgcolor = window.getComputedStyle(writemetacspframe.contentDocument.body) + .getPropertyValue("background-color"); + is(bgcolor, "transparent", "inital background value in FF should be 'transparent'"); + + // image: make sure image is blocked + var img = writemetacspframe.contentDocument.getElementById("testimage"); + is(img.width, 0, "image widht should be 0"); + is(img.height, 0, "image widht should be 0"); + + // script: make sure defined variable in external script is undefined + is(writemetacspframe.contentDocument.myMetaCSPScript, undefined, "myMetaCSPScript should be 'undefined'"); + + checkTestsDone(); +} + +// document.write(<--) to comment out meta csp should allow resources to be loaded +// after the preload failed +function checkResultsAllowed() { + commentmetacspframe.removeEventListener('load', checkResultsAllowed, false); + + // stylesheet: should be applied; bgcolor should be red + var bgcolor = window.getComputedStyle(commentmetacspframe.contentDocument.body).getPropertyValue("background-color"); + is(bgcolor, "rgb(255, 0, 0)", "background should be red/rgb(255, 0, 0)"); + + // image: should be completed + var img = commentmetacspframe.contentDocument.getElementById("testimage"); + ok(img.complete, "image should not be loaded"); + + // script: defined variable in external script should be accessible + is(commentmetacspframe.contentDocument.myMetaCSPScript, "external-JS-loaded", "myMetaCSPScript should be 'external-JS-loaded'"); + + checkTestsDone(); +} + +// doc.write(meta csp) should should allow preloads but should block actual loads +writemetacspframe.src = 'file_docwrite_meta.html'; +writemetacspframe.addEventListener('load', checkResultsBlocked, false); + +// commenting out a meta CSP should result in loaded image, script, style +commentmetacspframe.src = 'file_doccomment_meta.html'; +commentmetacspframe.addEventListener('load', checkResultsAllowed, false); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_dual_header.html b/dom/security/test/csp/test_dual_header.html new file mode 100644 index 000000000..6d3a35fd0 --- /dev/null +++ b/dom/security/test/csp/test_dual_header.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1036399 - Multiple CSP policies should be combined towards an intersection</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <p id="display"></p> + <div id="content" style="visibility: hidden"> + <iframe style="width:100%;" id="testframe"></iframe> + </div> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * We have two tests where each tests serves a page using two CSP policies: + * a) * default-src 'self' + * * default-src 'self' 'unsafe-inline' + * + * b) * default-src 'self' 'unsafe-inline' + * * default-src 'self' 'unsafe-inline' + * + * We make sure the inline script is *blocked* for test (a) but *allowed* for test (b). + * Multiple CSPs should be combined towards an intersection and it shouldn't be possible + * to open up (loosen) a CSP policy. + */ + +const TESTS = [ + { query: "tight", result: "blocked" }, + { query: "loose", result: "allowed" } +]; +var testCounter = -1; + +function ckeckResult() { + try { + document.getElementById("testframe").removeEventListener('load', ckeckResult, false); + var testframe = document.getElementById("testframe"); + var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML; + is(divcontent, curTest.result, "should be 'blocked'!"); + } + catch (e) { + ok(false, "error: could not access content in div container!"); + } + loadNextTest(); +} + +function loadNextTest() { + testCounter++; + if (testCounter >= TESTS.length) { + SimpleTest.finish(); + return; + } + curTest = TESTS[testCounter]; + var src = "file_dual_header_testserver.sjs?" + curTest.query; + document.getElementById("testframe").addEventListener("load", ckeckResult, false); + document.getElementById("testframe").src = src; +} + +SimpleTest.waitForExplicitFinish(); +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_evalscript.html b/dom/security/test/csp/test_evalscript.html new file mode 100644 index 000000000..f0ec3407c --- /dev/null +++ b/dom/security/test/csp/test_evalscript.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Content Security Policy "no eval" base restriction</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<iframe style="width:100%;height:300px;" id='cspframe'></iframe> +<iframe style="width:100%;height:300px;" id='cspframe2'></iframe> +<script class="testbody" type="text/javascript"> + +var evalScriptsThatRan = 0; +var evalScriptsBlocked = 0; +var evalScriptsTotal = 17; + +// called by scripts that run +var scriptRan = function(shouldrun, testname, data) { + evalScriptsThatRan++; + ok(shouldrun, 'EVAL SCRIPT RAN: ' + testname + '(' + data + ')'); + checkTestResults(); +} + +// called when a script is blocked +var scriptBlocked = function(shouldrun, testname, data) { + evalScriptsBlocked++; + ok(!shouldrun, 'EVAL SCRIPT BLOCKED: ' + testname + '(' + data + ')'); + checkTestResults(); +} + +var verifyZeroRetVal = function(val, testname) { + ok(val === 0, 'RETURN VALUE SHOULD BE ZERO, was ' + val + ': ' + testname); +} + +// Check to see if all the tests have run +var checkTestResults = function() { + // if any test is incomplete, keep waiting + if (evalScriptsTotal - evalScriptsBlocked - evalScriptsThatRan > 0) + return; + + // ... otherwise, finish + SimpleTest.finish(); +} + +////////////////////////////////////////////////////////////////////// +// set up and go +SimpleTest.waitForExplicitFinish(); + +// save this for last so that our listeners are registered. +// ... this loads the testbed of good and bad requests. +document.getElementById('cspframe').src = 'file_evalscript_main.html'; +document.getElementById('cspframe2').src = 'file_evalscript_main_allowed.html'; +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_fontloader.html b/dom/security/test/csp/test_fontloader.html new file mode 100644 index 000000000..cdb177f2a --- /dev/null +++ b/dom/security/test/csp/test_fontloader.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1122236 - CSP: Implement block-all-mixed-content</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <!-- Including WindowSnapshot.js so we can take screenshots of containers !--> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="setupTests()"> +<iframe style="width:100%;" id="baselineframe"></iframe> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the tests: + * We load a baselineFrame and compare the testFrame using + * compareSnapshots whether the font got loaded or blocked. + * Test 1: Use font-src 'none' so font gets blocked + * Test 2: Use font-src * so font gets loaded + * Test 3: Use no csp so font gets loaded + * Test 4: Use font-src 'none' so font gets blocked + * Makes sure the cache gets invalidated. + */ + +SimpleTest.waitForExplicitFinish(); + +const BASE_URI = "https://example.com/tests/dom/security/test/csp/"; + +const tests = [ + { // test 1 + query: "csp-block", + expected: true, // frames should be equal since font is *not* allowed to load + description: "font should be blocked by csp (csp-block)" + }, + { // test 2 + query: "csp-allow", + expected: false, // frames should *not* be equal since font is loaded + description: "font should load and apply (csp-allow)" + }, + { // test 3 + query: "no-csp", + expected: false, // frames should *not* be equals since font is loaded + description: "font should load and apply (no-csp)" + }, + { // test 4 + query: "csp-block", + expected: true, // frames should be equal since font is *not* allowed to load + description: "font should be blocked by csp (csp-block) [apply csp to cache]" + } +]; + +var curTest; +var counter = -1; +var baselineframe = document.getElementById("baselineframe"); +var testframe = document.getElementById("testframe"); + +function checkResult() { + testframe.removeEventListener('load', checkResult, false); + try { + ok(compareSnapshots(snapshotWindow(baselineframe.contentWindow), + snapshotWindow(testframe.contentWindow), + curTest.expected)[0], + curTest.description); + } catch(err) { + ok(false, "error: " + err.message); + } + loadNextTest(); +} + +function loadNextTest() { + counter++; + if (counter == tests.length) { + SimpleTest.finish(); + return; + } + curTest = tests[counter]; + testframe.addEventListener("load", checkResult, false); + testframe.src = BASE_URI + "file_fontloader.sjs?" + curTest.query; +} + +// once the baselineframe is loaded we can start running tests +function startTests() { + baselineframe.removeEventListener('load', startTests, false); + loadNextTest(); +} + +// make sure the main page is loaded before we start the test +function setupTests() { + baselineframe.addEventListener("load", startTests, false); + baselineframe.src = BASE_URI + "file_fontloader.sjs?baseline"; +} + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_form-action.html b/dom/security/test/csp/test_form-action.html new file mode 100644 index 000000000..b909ca701 --- /dev/null +++ b/dom/security/test/csp/test_form-action.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 529697 - Test mapping of form submission to form-action</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <p id="display"></p> + <div id="content" style="visibility: hidden"> + <iframe style="width:100%;" id="testframe"></iframe> + </div> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * We load a page with a given CSP and verify that form submissions are correctly + * evaluated through the "form-action" directive. + */ + +SimpleTest.waitForExplicitFinish(); + +var tests = [ + { + page : "file_form-action.html", + result : "allowed", + policy : "form-action 'self'" + }, + { + page : "file_form-action.html", + result : "blocked", + policy : "form-action 'none'" + } +]; + +// initializing to -1 so we start at index 0 when we start the test +var counter = -1; + +function checkResult(aResult) { + is(aResult, tests[counter].result, "should be " + tests[counter].result + " in test " + counter + "!"); + loadNextTest(); +} + +// We use the examiner to identify requests that hit the wire and requests +// that are blocked by CSP and bubble up the result to the including iframe +// document (parent). +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} +examiner.prototype = { + observe: function(subject, topic, data) { + if (topic === "specialpowers-http-notify-request") { + // making sure we do not bubble a result for something other + // then the request in question. + if (!data.includes("submit-form")) { + return; + } + checkResult("allowed"); + } + + if (topic === "csp-on-violate-policy") { + // making sure we do not bubble a result for something other + // then the request in question. + var asciiSpec = SpecialPowers.getPrivilegedProps( + SpecialPowers.do_QueryInterface(subject, "nsIURI"), + "asciiSpec"); + if (!asciiSpec.includes("submit-form")) { + return; + } + checkResult("blocked"); + } + }, + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} +window.FormActionExaminer = new examiner(); + +function loadNextTest() { + counter++; + if (counter == tests.length) { + window.FormActionExaminer.remove(); + SimpleTest.finish(); + return; + } + + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape("tests/dom/security/test/csp/" + tests[counter].page); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(tests[counter].policy); + + document.getElementById("testframe").src = src; +} + +// start running the tests +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_form_action_blocks_url.html b/dom/security/test/csp/test_form_action_blocks_url.html new file mode 100644 index 000000000..ef5c8d9b4 --- /dev/null +++ b/dom/security/test/csp/test_form_action_blocks_url.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<html> +<head> + <title>Bug 1251043 - Test form-action blocks URL</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <iframe id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> +/* + * Description of the test: + * 1) Let's load a form into an iframe which uses a CSP of: form-action 'none'; + * 2) Let's hit the submit button and make sure the form is not submitted. + * + * Since a blocked form submission does not fire any event handler, we have to + * use timeout triggered function that verifies that the form didn't get submitted. + */ + +SimpleTest.requestFlakyTimeout( + "Form submission blocked by CSP does not fire any events " + + "hence we have to check back after 300ms to make sure the form " + + "is not submitted"); +SimpleTest.waitForExplicitFinish(); + +const FORM_SUBMITTED = "form submission succeeded"; +var timeOutId; +var testframe = document.getElementById("testframe"); + +// In case the form gets submitted, the test would receive an 'load' +// event and would trigger the test to fail early. +function logFormSubmittedError() { + clearTimeout(timeOutId); + testframe.removeEventListener('load', logFormSubmittedError, false); + ok(false, "form submission should be blocked"); + SimpleTest.finish(); +} + +// After 300ms we verify the form did not get submitted. +function verifyFormNotSubmitted() { + clearTimeout(timeOutId); + var frameContent = testframe.contentWindow.document.body.innerHTML; + isnot(frameContent.indexOf("CONTROL-TEXT"), -1, + "form should not be submitted and still contain the control text"); + SimpleTest.finish(); +} + +function submitForm() { + // Part 1: The form has loaded in the testframe + // unregister the current event handler + testframe.removeEventListener('load', submitForm, false); + + // Part 2: Register a new load event handler. In case the + // form gets submitted, this load event fires and we can + // fail the test right away. + testframe.addEventListener("load", logFormSubmittedError, false); + + // Part 3: Since blocking the form does not throw any kind of error; + // Firefox just logs the CSP error to the console we have to register + // this timeOut function which then verifies that the form didn't + // get submitted. + timeOutId = setTimeout(verifyFormNotSubmitted, 300); + + // Part 4: We are ready, let's hit the submit button of the form. + var submitButton = testframe.contentWindow.document.getElementById('submitButton'); + submitButton.click(); +} + +testframe.addEventListener("load", submitForm, false); +testframe.src = "file_form_action_server.sjs?loadframe"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_frameancestors.html b/dom/security/test/csp/test_frameancestors.html new file mode 100644 index 000000000..8ccbc8156 --- /dev/null +++ b/dom/security/test/csp/test_frameancestors.html @@ -0,0 +1,157 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Content Security Policy Frame Ancestors directive</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<iframe style="width:100%;height:300px;" id='cspframe'></iframe> +<script class="testbody" type="text/javascript"> + +// These are test results: -1 means it hasn't run, +// true/false is the pass/fail result. +var framesThatShouldLoad = { + aa_allow: -1, /* innermost frame allows a * + //aa_block: -1, /* innermost frame denies a */ + ab_allow: -1, /* innermost frame allows a */ + //ab_block: -1, /* innermost frame denies a */ + aba_allow: -1, /* innermost frame allows b,a */ + //aba_block: -1, /* innermost frame denies b */ + //aba2_block: -1, /* innermost frame denies a */ + abb_allow: -1, /* innermost frame allows b,a */ + //abb_block: -1, /* innermost frame denies b */ + //abb2_block: -1, /* innermost frame denies a */ +}; + +// we normally expect _6_ violations (6 test cases that cause blocks), but many +// of the cases cause violations due to the // origin of the test harness (same +// as 'a'). When the violation is cross-origin, the URI passed to the observers +// is null (see bug 846978). This means we can't tell if it's part of the test +// case or if it is the test harness frame (also origin 'a'). +// As a result, we'll get an extra violation for the following cases: +// ab_block "frame-ancestors 'none'" (outer frame and test harness) +// aba2_block "frame-ancestors b" (outer frame and test harness) +// abb2_block "frame-ancestors b" (outer frame and test harness) +// +// and while we can detect the test harness check for this one case since +// the violations are not cross-origin and we get the URI: +// aba2_block "frame-ancestors b" (outer frame and test harness) +// +// we can't for these other ones: +// ab_block "frame-ancestors 'none'" (outer frame and test harness) +// abb2_block "frame-ancestors b" (outer frame and test harness) +// +// so that results in 2 extra violation notifications due to the test harness. +// expected = 6, total = 8 +// +// Number of tests that pass for this file should be 12 (8 violations 4 loads) +var expectedViolationsLeft = 8; + +// This is used to watch the blocked data bounce off CSP and allowed data +// get sent out to the wire. +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); +} +examiner.prototype = { + observe: function(subject, topic, data) { + // subject should be an nsURI... though could be null since CSP + // prohibits cross-origin URI reporting during frame ancestors checks. + if (subject && !SpecialPowers.can_QI(subject)) + return; + + var asciiSpec = subject; + + try { + asciiSpec = SpecialPowers.getPrivilegedProps( + SpecialPowers.do_QueryInterface(subject, "nsIURI"), + "asciiSpec"); + + // skip checks on the test harness -- can't do this skipping for + // cross-origin blocking since the observer doesn't get the URI. This + // can cause this test to over-succeed (but only in specific cases). + if (asciiSpec.includes("test_frameancestors.html")) { + return; + } + } catch (ex) { + // was not an nsIURI, so it was probably a cross-origin report. + } + + + if (topic === "csp-on-violate-policy") { + //these were blocked... record that they were blocked + window.frameBlocked(asciiSpec, data); + } + }, + + // must eventually call this to remove the listener, + // or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + } +} + +// called when a frame is loaded +// -- if it's not enumerated above, it should not load! +var frameLoaded = function(testname, uri) { + //test already complete.... forget it... remember the first result. + if (window.framesThatShouldLoad[testname] != -1) + return; + + if (typeof window.framesThatShouldLoad[testname] === 'undefined') { + // uh-oh, we're not expecting this frame to load! + ok(false, testname + ' framed site should not have loaded: ' + uri); + } else { + framesThatShouldLoad[testname] = true; + ok(true, testname + ' framed site when allowed by csp (' + uri + ')'); + } + checkTestResults(); +} + +// called when a frame is blocked +// -- we can't determine *which* frame was blocked, but at least we can count them +var frameBlocked = function(uri, policy) { + ok(true, 'a CSP policy blocked frame from being loaded: ' + policy); + expectedViolationsLeft--; + checkTestResults(); +} + + +// Check to see if all the tests have run +var checkTestResults = function() { + // if any test is incomplete, keep waiting + for (var v in framesThatShouldLoad) + if(window.framesThatShouldLoad[v] == -1) + return; + + if (window.expectedViolationsLeft > 0) + return; + + // ... otherwise, finish + window.examiner.remove(); + SimpleTest.finish(); +} + +window.addEventListener("message", receiveMessage, false); + +function receiveMessage(event) { + if (event.data.call && event.data.call == 'frameLoaded') + frameLoaded(event.data.testname, event.data.uri); +} + +////////////////////////////////////////////////////////////////////// +// set up and go +window.examiner = new examiner(); +SimpleTest.waitForExplicitFinish(); + +// save this for last so that our listeners are registered. +// ... this loads the testbed of good and bad requests. +document.getElementById('cspframe').src = 'file_frameancestors_main.html'; + +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_hash_source.html b/dom/security/test/csp/test_hash_source.html new file mode 100644 index 000000000..6dde11dac --- /dev/null +++ b/dom/security/test/csp/test_hash_source.html @@ -0,0 +1,135 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test CSP 1.1 hash-source for inline scripts and styles</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="visibility:hidden"> + <iframe style="width:100%;" id='cspframe'></iframe> +</div> +<script class="testbody" type="text/javascript"> + +function cleanup() { + // finish the tests + SimpleTest.finish(); +} + +function checkInline () { + var cspframe = document.getElementById('cspframe').contentDocument; + + var inlineScriptTests = { + 'inline-script-valid-hash': { + shouldBe: 'allowed', + message: 'Inline script with valid hash should be allowed' + }, + 'inline-script-invalid-hash': { + shouldBe: 'blocked', + message: 'Inline script with invalid hash should be blocked' + }, + 'inline-script-invalid-hash-valid-nonce': { + shouldBe: 'allowed', + message: 'Inline script with invalid hash and valid nonce should be allowed' + }, + 'inline-script-valid-hash-invalid-nonce': { + shouldBe: 'allowed', + message: 'Inline script with valid hash and invalid nonce should be allowed' + }, + 'inline-script-invalid-hash-invalid-nonce': { + shouldBe: 'blocked', + message: 'Inline script with invalid hash and invalid nonce should be blocked' + }, + 'inline-script-valid-sha512-hash': { + shouldBe: 'allowed', + message: 'Inline script with a valid sha512 hash should be allowed' + }, + 'inline-script-valid-sha384-hash': { + shouldBe: 'allowed', + message: 'Inline script with a valid sha384 hash should be allowed' + }, + 'inline-script-valid-sha1-hash': { + shouldBe: 'blocked', + message: 'Inline script with a valid sha1 hash should be blocked, because sha1 is not a valid hash function' + }, + 'inline-script-valid-md5-hash': { + shouldBe: 'blocked', + message: 'Inline script with a valid md5 hash should be blocked, because md5 is not a valid hash function' + } + } + + for (testId in inlineScriptTests) { + var test = inlineScriptTests[testId]; + is(cspframe.getElementById(testId).innerHTML, test.shouldBe, test.message); + } + + // Inline style tries to change an element's color to green. If blocked, the + // element's color will be the default black. + var green = "rgb(0, 128, 0)"; + var black = "rgb(0, 0, 0)"; + + var getElementColorById = function (id) { + return window.getComputedStyle(cspframe.getElementById(id), null).color; + }; + + var inlineStyleTests = { + 'inline-style-valid-hash': { + shouldBe: green, + message: 'Inline style with valid hash should be allowed' + }, + 'inline-style-invalid-hash': { + shouldBe: black, + message: 'Inline style with invalid hash should be blocked' + }, + 'inline-style-invalid-hash-valid-nonce': { + shouldBe: green, + message: 'Inline style with invalid hash and valid nonce should be allowed' + }, + 'inline-style-valid-hash-invalid-nonce': { + shouldBe: green, + message: 'Inline style with valid hash and invalid nonce should be allowed' + }, + 'inline-style-invalid-hash-invalid-nonce' : { + shouldBe: black, + message: 'Inline style with invalid hash and invalid nonce should be blocked' + }, + 'inline-style-valid-sha512-hash': { + shouldBe: green, + message: 'Inline style with a valid sha512 hash should be allowed' + }, + 'inline-style-valid-sha384-hash': { + shouldBe: green, + message: 'Inline style with a valid sha384 hash should be allowed' + }, + 'inline-style-valid-sha1-hash': { + shouldBe: black, + message: 'Inline style with a valid sha1 hash should be blocked, because sha1 is not a valid hash function' + }, + 'inline-style-valid-md5-hash': { + shouldBe: black, + message: 'Inline style with a valid md5 hash should be blocked, because md5 is not a valid hash function' + } + } + + for (testId in inlineStyleTests) { + var test = inlineStyleTests[testId]; + is(getElementColorById(testId), test.shouldBe, test.message); + } + + cleanup(); +} + +////////////////////////////////////////////////////////////////////// +// set up and go +SimpleTest.waitForExplicitFinish(); + +// save this for last so that our listeners are registered. +// ... this loads the testbed of good and bad requests. +document.getElementById('cspframe').src = 'file_hash_source.html'; +document.getElementById('cspframe').addEventListener('load', checkInline, false); +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_iframe_sandbox.html b/dom/security/test/csp/test_iframe_sandbox.html new file mode 100644 index 000000000..f79f94135 --- /dev/null +++ b/dom/security/test/csp/test_iframe_sandbox.html @@ -0,0 +1,239 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=671389 +Bug 671389 - Implement CSP sandbox directive +--> +<head> + <meta charset="utf-8"> + <title>Tests for Bug 671389</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script type="application/javascript"> + + SimpleTest.waitForExplicitFinish(); + + // Check if two sandbox flags are the same, ignoring case-sensitivity. + // getSandboxFlags returns a list of sandbox flags (if any) or + // null if the flag is not set. + // This function checks if two flags are the same, i.e., they're + // either not set or have the same flags. + function eqFlags(a, b) { + if (a === null && b === null) { return true; } + if (a === null || b === null) { return false; } + if (a.length !== b.length) { return false; } + var a_sorted = a.map(function(e) { return e.toLowerCase(); }).sort(); + var b_sorted = b.map(function(e) { return e.toLowerCase(); }).sort(); + for (var i in a_sorted) { + if (a_sorted[i] !== b_sorted[i]) { + return false; + } + } + return true; + } + + // Get the sandbox flags of document doc. + // If the flag is not set sandboxFlagsAsString returns null, + // this function also returns null. + // If the flag is set it may have some flags; in this case + // this function returns the (potentially empty) list of flags. + function getSandboxFlags(doc) { + var flags = doc.sandboxFlagsAsString; + if (flags === null) { return null; } + return flags? flags.split(" "):[]; + } + + // Constructor for a CSP sandbox flags test. The constructor + // expectes a description 'desc' and set of options 'opts': + // - sandboxAttribute: [null] or string corresponding to the iframe sandbox attributes + // - csp: [null] or string corresponding to the CSP sandbox flags + // - cspReportOnly: [null] or string corresponding to the CSP report-only sandbox flags + // - file: [null] or string corresponding to file the server should serve + // Above, we use [brackets] to denote default values. + function CSPFlagsTest(desc, opts) { + function ifundef(x, v) { + return (x !== undefined) ? x : v; + } + + function intersect(as, bs) { // Intersect two csp attributes: + as = as === null ? null + : as.split(' ').filter(function(x) { return !!x; }); + bs = bs === null ? null + : bs.split(' ').filter(function(x) { return !!x; }); + + if (as === null) { return bs; } + if (bs === null) { return as; } + + var cs = []; + as.forEach(function(a) { + if (a && bs.indexOf(a) != -1) + cs.push(a); + }); + return cs; + } + + this.desc = desc || "Untitled test"; + this.attr = ifundef(opts.sandboxAttribute, null); + this.csp = ifundef(opts.csp, null); + this.cspRO = ifundef(opts.cspReportOnly, null); + this.file = ifundef(opts.file, null); + this.expected = intersect(this.attr, this.csp); + } + + // Return function that checks that the actual flags are the same as the + // expected flags + CSPFlagsTest.prototype.checkFlags = function(iframe) { + var this_ = this; + return function() { + try { + var actual = getSandboxFlags(SpecialPowers.wrap(iframe).contentDocument); + ok(eqFlags(actual, this_.expected), + this_.desc, 'expected: "' + this_.expected + '", got: "' + actual + '"'); + } catch (e) { + ok(false, this_.desc, 'expected: "' + this_.expected + '", failed with: "' + e + '"'); + } + runNextTest(); + }; + }; + + // Set the iframe src and sandbox attribute + CSPFlagsTest.prototype.runTest = function () { + var iframe = document.createElement('iframe'); + document.getElementById("content").appendChild(iframe); + iframe.onload = this.checkFlags(iframe); + + // set sandbox attribute + if (this.attr === null) { + iframe.removeAttribute('sandbox'); + } else { + iframe.sandbox = this.attr; + } + + // set query string + var src = 'http://mochi.test:8888/tests/dom/security/test/csp/file_testserver.sjs'; + + var delim = '?'; + + if (this.csp !== null) { + src += delim + 'csp=' + escape('sandbox ' + this.csp); + delim = '&'; + } + + if (this.cspRO !== null) { + src += delim + 'cspRO=' + escape('sandbox ' + this.cspRO); + delim = '&'; + } + + if (this.file !== null) { + src += delim + 'file=' + escape(this.file); + delim = '&'; + } + + iframe.src = src; + iframe.width = iframe.height = 10; + + } + + testCases = [ + { + desc: "Test 1: Header should not override attribute", + sandboxAttribute: "", + csp: "allow-forms aLLOw-POinter-lock alLOW-popups aLLOW-SAME-ORIGin ALLOW-SCRIPTS allow-top-navigation" + }, + { + desc: "Test 2: Attribute should not override header", + sandboxAttribute: "sandbox allow-forms allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation", + csp: "" + }, + { + desc: "Test 3: Header and attribute intersect", + sandboxAttribute: "allow-same-origin allow-scripts", + csp: "allow-forms allow-same-origin allow-scripts" + }, + { + desc: "Test 4: CSP sandbox sets the right flags (pt 1)", + csp: "alLOW-FORms ALLOW-pointer-lock allow-popups allow-same-origin allow-scripts ALLOW-TOP-NAVIGation" + }, + { + desc: "Test 5: CSP sandbox sets the right flags (pt 2)", + csp: "allow-same-origin allow-TOP-navigation" + }, + { + desc: "Test 6: CSP sandbox sets the right flags (pt 3)", + csp: "allow-FORMS ALLOW-scripts" + }, + { + desc: "Test 7: CSP sandbox sets the right flags (pt 4)", + csp: "" + }, + { + desc: "Test 8: CSP sandbox sets the right flags (pt 5)", + csp: null + }, + { + desc: "Test 9: Read-only header should not override attribute", + sandboxAttribute: "", + cspReportOnly: "allow-forms ALLOW-pointer-lock allow-POPUPS allow-same-origin ALLOW-scripts allow-top-NAVIGATION" + }, + { + desc: "Test 10: Read-only header should not override CSP header", + csp: "allow-forms allow-scripts", + cspReportOnly: "allow-forms aLlOw-PoInTeR-lOcK aLLow-pOPupS aLLoW-SaME-oRIgIN alLow-scripts allow-tOp-navigation" + }, + { + desc: "Test 11: Read-only header should not override attribute or CSP header", + sandboxAttribute: "allow-same-origin allow-scripts", + csp: "allow-forms allow-same-origin allow-scripts", + cspReportOnly: "allow-forms allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation" + }, + { + desc: "Test 12: CSP sandbox not affected by document.write()", + csp: "allow-scripts", + file: 'tests/dom/security/test/csp/file_iframe_sandbox_document_write.html' + }, + ].map(function(t) { return (new CSPFlagsTest(t.desc,t)); }); + + + var testCaseIndex = 0; + + // Track ok messages from iframes + var childMessages = 0; + var totalChildMessages = 1; + + + // Check to see if we ran all the tests and received all messges + // from child iframes. If so, finish. + function tryFinish() { + if (testCaseIndex === testCases.length && childMessages === totalChildMessages){ + SimpleTest.finish(); + } + } + + function runNextTest() { + + tryFinish(); + + if (testCaseIndex < testCases.length) { + testCases[testCaseIndex].runTest(); + testCaseIndex++; + } + } + + function receiveMessage(event) { + ok(event.data.ok, event.data.desc); + childMessages++; + tryFinish(); + } + + window.addEventListener("message", receiveMessage, false); + + addLoadEvent(runNextTest); +</script> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=671389">Mozilla Bug 671389</a> - Implement CSP sandbox directive + <p id="display"></p> + <div id="content"> + </div> +</body> +</html> diff --git a/dom/security/test/csp/test_iframe_sandbox_srcdoc.html b/dom/security/test/csp/test_iframe_sandbox_srcdoc.html new file mode 100644 index 000000000..53beafcac --- /dev/null +++ b/dom/security/test/csp/test_iframe_sandbox_srcdoc.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1073952 - CSP should restrict scripts in srcdoc iframe even if sandboxed</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display">Bug 1073952</p> +<iframe style="width:200px;height:200px;" id='cspframe'></iframe> +<script class="testbody" type="text/javascript"> + +// This is used to watch the blocked data bounce off CSP and allowed data +// get sent out to the wire. +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); +} + +examiner.prototype = { + observe: function(subject, topic, data) { + + if(topic === "csp-on-violate-policy") { + var violationString = SpecialPowers.getPrivilegedProps(SpecialPowers. + do_QueryInterface(subject, "nsISupportsCString"), "data"); + // the violation subject for inline script violations is unfortunately vague, + // all we can do is match the string. + if (!violationString.includes("Inline Script")) { + return + } + ok(true, "CSP inherited into sandboxed srcdoc iframe, script blocked."); + window.testFinished(); + } + }, + + // must eventually call this to remove the listener, + // or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + } +} + +window.examiner = new examiner(); + +function testFinished() { + window.examiner.remove(); + SimpleTest.finish(); +} + +addEventListener("message", function(e) { + ok(false, "We should not execute JS in srcdoc iframe."); + window.testFinished(); +}) +SimpleTest.waitForExplicitFinish(); + +// save this for last so that our listeners are registered. +// ... this loads the testbed of good and bad requests. +document.getElementById('cspframe').src = 'file_iframe_sandbox_srcdoc.html'; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_iframe_sandbox_top_1.html b/dom/security/test/csp/test_iframe_sandbox_top_1.html new file mode 100644 index 000000000..d9ba71824 --- /dev/null +++ b/dom/security/test/csp/test_iframe_sandbox_top_1.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=671389 +Bug 671389 - Implement CSP sandbox directive + +Tests CSP sandbox attribute on top-level page. + +Minimal flags: allow-same-origin allow-scripts: +Since we need to load the SimpleTest files, we have to set the +allow-same-origin flag. Additionally, we set the allow-scripts flag +since we need JS to check the flags. + +Though not necessary, for this test we also set the allow-forms flag. +We may later wish to extend the testing suite with sandbox_csp_top_* +tests that set different permutations of the flags. + +CSP header: Content-Security-Policy: sandbox allow-forms allow-scripts allow-same-origin +--> +<head> + <meta charset="utf-8"> + <title>Tests for Bug 671389</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +// Check if two sandbox flags are the same. +// getSandboxFlags returns a list of sandbox flags (if any) or +// null if the flag is not set. +// This function checks if two flags are the same, i.e., they're +// either not set or have the same flags. +function eqFlags(a, b) { + if (a === null && b === null) { return true; } + if (a === null || b === null) { return false; } + if (a.length !== b.length) { return false; } + var a_sorted = a.sort(); + var b_sorted = b.sort(); + for (var i in a_sorted) { + if (a_sorted[i] !== b_sorted[i]) { + return false; + } + } + return true; +} + +// Get the sandbox flags of document doc. +// If the flag is not set sandboxFlagsAsString returns null, +// this function also returns null. +// If the flag is set it may have some flags; in this case +// this function returns the (potentially empty) list of flags. +function getSandboxFlags(doc) { + var flags = doc.sandboxFlagsAsString; + if (flags === null) { return null; } + return flags? flags.split(" "):[]; +} + +function checkFlags(expected) { + try { + var flags = getSandboxFlags(SpecialPowers.wrap(document)); + ok(eqFlags(flags, expected), name + ' expected: "' + expected + '", got: "' + flags + '"'); + } catch (e) { + ok(false, name + ' expected "' + expected + ', but failed with ' + e); + } + SimpleTest.finish(); +} + +</script> + +<body onLoad='checkFlags(["allow-forms", "allow-scripts", "allow-same-origin"]);'> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=671389">Mozilla Bug 671389</a> - Implement CSP sandbox directive +<p id="display"></p> +<div id="content"> + I am a top-level page sandboxed with "allow-scripts allow-forms + allow-same-origin". +</div> +</body> +</html> diff --git a/dom/security/test/csp/test_iframe_sandbox_top_1.html^headers^ b/dom/security/test/csp/test_iframe_sandbox_top_1.html^headers^ new file mode 100644 index 000000000..d9cd0606e --- /dev/null +++ b/dom/security/test/csp/test_iframe_sandbox_top_1.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: sAnDbOx aLLow-FOrms aLlOw-ScRiPtS ALLOW-same-origin diff --git a/dom/security/test/csp/test_iframe_srcdoc.html b/dom/security/test/csp/test_iframe_srcdoc.html new file mode 100644 index 000000000..95b924a5e --- /dev/null +++ b/dom/security/test/csp/test_iframe_srcdoc.html @@ -0,0 +1,140 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1073952 - Test CSP enforcement within iframe srcdoc</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * (1) We serve a site which makes use of script-allowed sandboxed iframe srcdoc + * and make sure that CSP applies to the nested browsing context + * within the iframe. + * [PAGE WITH CSP [IFRAME SANDBOX SRCDOC [SCRIPT]]] + * + * (2) We serve a site which nests script within an script-allowed sandboxed + * iframe srcdoc within another script-allowed sandboxed iframe srcdoc and + * make sure that CSP applies to the nested browsing context + * within the iframe*s*. + * [PAGE WITH CSP [IFRAME SANDBOX SRCDOC [IFRAME SANDBOX SRCDOC [SCRIPT]]]] + * + * Please note that the test relies on the "csp-on-violate-policy" observer. + * Whenever the script within the iframe is blocked observers are notified. + * In turn, this renders the 'result' within tests[] unused. In case the script + * would execute however, the postMessageHandler would bubble up 'allowed' and + * the test would fail. + */ + +SimpleTest.waitForExplicitFinish(); + +var tests = [ + // [PAGE *WITHOUT* CSP [IFRAME SRCDOC [SCRIPT]]] + { csp: "", + result: "allowed", + query: "simple_iframe_srcdoc", + desc: "No CSP should run script within script-allowed sandboxed iframe srcdoc" + }, + { csp: "script-src https://test1.com", + result: "blocked", + query: "simple_iframe_srcdoc", + desc: "CSP should block script within script-allowed sandboxediframe srcdoc" + }, + // [PAGE *WITHOUT* CSP [IFRAME SRCDOC [IFRAME SRCDOC [SCRIPT]]]] + { csp: "", + result: "allowed", + query: "nested_iframe_srcdoc", + desc: "No CSP should run script within script-allowed sandboxed iframe srcdoc nested within another script-allowed sandboxed iframe srcdoc" + }, + // [PAGE WITH CSP [IFRAME SRCDOC ]] + { csp: "script-src https://test2.com", + result: "blocked", + query: "nested_iframe_srcdoc", + desc: "CSP should block script within script-allowed sandboxed iframe srcdoc nested within another script-allowed sandboxed iframe srcdoc" + }, + { csp: "", + result: "allowed", + query: "nested_iframe_srcdoc_datauri", + desc: "No CSP, should run script within script-allowed sandboxed iframe src with data URL nested within another script-allowed sandboxed iframe srcdoc" + }, + { csp: "script-src https://test3.com", + result: "blocked", + query: "nested_iframe_srcdoc_datauri", + desc: "CSP should block script within script-allowed sandboxed iframe src with data URL nested within another script-allowed sandboxed iframe srcdoc" + }, + +]; + +// initializing to -1 so we start at index 0 when we start the test +var counter = -1; + +function finishTest() { + window.removeEventListener("message", receiveMessage, false); + window.examiner.remove(); + SimpleTest.finish(); +} + +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + var result = event.data.result; + testComplete(result, tests[counter].result, tests[counter].desc); +} + +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); +} + +examiner.prototype = { + observe: function(subject, topic, data) { + if (topic === "csp-on-violate-policy") { + var violationString = SpecialPowers.getPrivilegedProps(SpecialPowers. + do_QueryInterface(subject, "nsISupportsCString"), "data"); + // the violation subject for inline script violations is unfortunately vague, + // all we can do is match the string. + if (!violationString.includes("Inline Script")) { + return + } + testComplete("blocked", tests[counter].result, tests[counter].desc); + } + }, + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + } +} + +function testComplete(result, expected, desc) { + is(result, expected, desc); + // ignore cases when we get csp violations and postMessage from the same frame. + var frameURL = new URL(document.getElementById("testframe").src); + var params = new URLSearchParams(frameURL.search); + var counterInFrame = params.get("counter"); + if (counterInFrame == counter) { + loadNextTest(); + } +} + +function loadNextTest() { + counter++; + if (counter == tests.length) { + finishTest(); + return; + } + var src = "file_iframe_srcdoc.sjs"; + src += "?csp=" + escape(tests[counter].csp); + src += "&action=" + escape(tests[counter].query); + src += "&counter=" + counter; + document.getElementById("testframe").src = src; +} + +// start running the tests +window.examiner = new examiner(); +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_ignore_unsafe_inline.html b/dom/security/test/csp/test_ignore_unsafe_inline.html new file mode 100644 index 000000000..50f9cbb8d --- /dev/null +++ b/dom/security/test/csp/test_ignore_unsafe_inline.html @@ -0,0 +1,122 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1004703 - ignore 'unsafe-inline' if nonce- or hash-source specified</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +/* Description of the test: + * We load a page that contains three scripts using different policies + * and make sure 'unsafe-inline' is ignored within script-src if hash-source + * or nonce-source is specified. + * + * The expected output of each test is a sequence of chars. + * E.g. the default char we expect is 'a', depending on what inline scripts + * are allowed to run we also expect 'b', 'c', 'd'. + * + * The test also covers the handling of multiple policies where the second + * policy makes use of a directive that should *not* fallback to + * default-src, see Bug 1198422. + */ + +const POLICY_PREFIX = "default-src 'none'; script-src "; + +var tests = [ + { + policy1: POLICY_PREFIX + "'unsafe-inline'", + policy2: "frame-ancestors 'self'", + description: "'unsafe-inline' allows all scripts to execute", + file: "file_ignore_unsafe_inline.html", + result: "abcd", + }, + { + policy1: POLICY_PREFIX + "'unsafe-inline' 'sha256-uJXAPKP5NZxnVMZMUkDofh6a9P3UMRc1CRTevVPS/rI='", + policy2: "base-uri http://mochi.test", + description: "defining a hash should only allow one script to execute", + file: "file_ignore_unsafe_inline.html", + result: "ac", + }, + { + policy1: POLICY_PREFIX + "'unsafe-inline' 'nonce-FooNonce'", + policy2: "form-action 'none'", + description: "defining a nonce should only allow one script to execute", + file: "file_ignore_unsafe_inline.html", + result: "ad", + }, + { + policy1: POLICY_PREFIX + "'unsafe-inline' 'sha256-uJXAPKP5NZxnVMZMUkDofh6a9P3UMRc1CRTevVPS/rI=' 'nonce-FooNonce'", + policy2: "upgrade-insecure-requests", + description: "defining hash and nonce should allow two scripts to execute", + file: "file_ignore_unsafe_inline.html", + result: "acd", + }, + { + policy1: POLICY_PREFIX + "'unsafe-inline' 'sha256-uJXAPKP5NZxnVMZMUkDofh6a9P3UMRc1CRTevVPS/rI=' 'nonce-FooNonce' 'unsafe-inline'", + policy2: "referrer origin", + description: "defining hash, nonce and 'unsafe-inline' twice should still only allow two scripts to execute", + file: "file_ignore_unsafe_inline.html", + result: "acd", + }, + { + policy1: "default-src 'unsafe-inline' 'sha256-uJXAPKP5NZxnVMZMUkDofh6a9P3UMRc1CRTevVPS/rI=' 'nonce-FooNonce' ", + policy2: "sandbox allow-scripts allow-same-origin", + description: "unsafe-inline should *not* be ignored within default-src even if hash or nonce is specified", + file: "file_ignore_unsafe_inline.html", + result: "abcd", + }, +]; + +var counter = 0; +var curTest; + +function loadNextTest() { + if (counter == tests.length) { + document.getElementById("testframe").removeEventListener("load", test, false); + SimpleTest.finish(); + return; + } + + curTest = tests[counter++]; + var src = "file_ignore_unsafe_inline_multiple_policies_server.sjs?file="; + // append the file that should be served + src += escape("tests/dom/security/test/csp/" + curTest.file); + + // append the first CSP that should be used to serve the file + src += "&csp1=" + escape(curTest.policy1); + // append the second CSP that should be used to serve the file + src += "&csp2=" + escape(curTest.policy2); + + document.getElementById("testframe").addEventListener("load", test, false); + document.getElementById("testframe").src = src; +} + +function test() { + try { + document.getElementById("testframe").removeEventListener('load', test, false); + var testframe = document.getElementById("testframe"); + var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML; + // sort the characters to make sure the result is in ascending order + // in case handlers run out of order + divcontent = divcontent.split('').sort().join(''); + + is(divcontent, curTest.result, curTest.description); + } + catch (e) { + ok(false, "error: could not access content for test " + curTest.description + "!"); + } + loadNextTest(); +} + +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_inlinescript.html b/dom/security/test/csp/test_inlinescript.html new file mode 100644 index 000000000..7fd5695d5 --- /dev/null +++ b/dom/security/test/csp/test_inlinescript.html @@ -0,0 +1,123 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Content Security Policy Frame Ancestors directive</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<iframe style="width:100%;height:300px;" id='testframe'></iframe> + +<script class="testbody" type="text/javascript"> + +var tests = [ + { + /* test allowed */ + csp: "default-src 'self'; script-src 'self' 'unsafe-inline'", + results: ["body-onload-fired", "text-node-fired", + "javascript-uri-fired", "javascript-uri-anchor-fired"], + desc: "allow inline scripts", + received: 0, // counter to make sure we received all 4 reports + }, + { + /* test blocked */ + csp: "default-src 'self'", + results: ["inline-script-blocked"], + desc: "block inline scripts", + received: 0, // counter to make sure we received all 4 reports + } +]; + +var counter = 0; +var curTest; + +// This is used to watch the blocked data bounce off CSP and allowed data +// get sent out to the wire. +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); +} +examiner.prototype = { + observe: function(subject, topic, data) { + if (topic !== "csp-on-violate-policy") { + return; + } + + var what = SpecialPowers.getPrivilegedProps(SpecialPowers. + do_QueryInterface(subject, "nsISupportsCString"), "data"); + + if (!what.includes("Inline Script had invalid hash") && + !what.includes("Inline Scripts will not execute")) { + return; + } + window.checkResults("inline-script-blocked"); + }, + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + } +} + +function finishTest() { + window.examiner.remove(); + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); +} + +// Check to see if all the tests have run +var checkResults = function(result) { + var index = curTest.results.indexOf(result); + isnot(index, -1, "should find result (" + result +") within test: " + curTest.desc); + if (index > -1) { + curTest.received += 1; + } + + // make sure we receive all the 4 reports for the 4 inline scripts + if (curTest.received < 4) { + return; + } + + if (counter < tests.length) { + loadNextTest(); + return; + } + finishTest(); +} + +// a postMessage handler that is used to bubble up results from the testframe +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + checkResults(event.data); +} + +function clickit() { + document.getElementById("testframe").removeEventListener('load', clickit, false); + var testframe = document.getElementById('testframe'); + var a = testframe.contentDocument.getElementById('anchortoclick'); + sendMouseEvent({type:'click'}, a, testframe.contentWindow); +} + +function loadNextTest() { + curTest = tests[counter++]; + var src = "file_testserver.sjs?file="; + // append the file that should be served + src += escape("tests/dom/security/test/csp/file_inlinescript.html"); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(curTest.csp); + + document.getElementById("testframe").src = src; + document.getElementById("testframe").addEventListener("load", clickit, false); +} + +// set up the test and go +window.examiner = new examiner(); +SimpleTest.waitForExplicitFinish(); +loadNextTest(); + +</script> + +</body> +</html> diff --git a/dom/security/test/csp/test_inlinestyle.html b/dom/security/test/csp/test_inlinestyle.html new file mode 100644 index 000000000..eadcf1e38 --- /dev/null +++ b/dom/security/test/csp/test_inlinestyle.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Content Security Policy inline stylesheets stuff</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<iframe style="width:100%;height:300px;" id='cspframe1'></iframe> +<iframe style="width:100%;height:300px;" id='cspframe2'></iframe> +<script class="testbody" type="text/javascript"> + +////////////////////////////////////////////////////////////////////// +// set up and go +SimpleTest.waitForExplicitFinish(); + +var done = 0; + +// When a CSP 1.0 compliant policy is specified we should block inline +// styles applied by <style> element, style attribute, and SMIL <animate> and <set> tags +// (when it's not explicitly allowed.) +function checkStyles(evt) { + var cspframe = document.getElementById('cspframe1'); + var color; + + // black means the style wasn't applied. green colors are used for styles + //expected to be applied. A color is red if a style is erroneously applied + color = window.getComputedStyle(cspframe.contentDocument.getElementById('linkstylediv'),null)['color']; + ok('rgb(0, 255, 0)' === color, 'External Stylesheet (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('inlinestylediv'),null)['color']; + ok('rgb(0, 0, 0)' === color, 'Inline Style TAG (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('attrstylediv'),null)['color']; + ok('rgb(0, 0, 0)' === color, 'Style Attribute (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('csstextstylediv'),null)['color']; + ok('rgb(0, 255, 0)' === color, 'cssText (' + color + ')'); + // SMIL tests + color = window.getComputedStyle(cspframe.contentDocument.getElementById('xmlTest',null))['fill']; + ok('rgb(0, 0, 0)' === color, 'XML Attribute styling (SMIL) (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('cssOverrideTest',null))['fill']; + ok('rgb(0, 0, 0)' === color, 'CSS Override styling (SMIL) (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('cssOverrideTestById',null))['fill']; + ok('rgb(0, 0, 0)' === color, 'CSS Override styling via ID lookup (SMIL) (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('cssSetTestById',null))['fill']; + ok('rgb(0, 0, 0)' === color, 'CSS Set Element styling via ID lookup (SMIL) (' + color + ')'); + + color = window.getComputedStyle(cspframe.contentDocument.getElementById('modifycsstextdiv'),null)['color']; + ok('rgb(0, 255, 0)' === color, 'Modify loaded style sheet via cssText (' + color + ')'); + + checkIfDone(); +} + +// When a CSP 1.0 compliant policy is specified we should allow inline +// styles when it is explicitly allowed. +function checkStylesAllowed(evt) { + var cspframe = document.getElementById('cspframe2'); + var color; + + // black means the style wasn't applied. green colors are used for styles + // expected to be applied. A color is red if a style is erroneously applied + color = window.getComputedStyle(cspframe.contentDocument.getElementById('linkstylediv'),null)['color']; + ok('rgb(0, 255, 0)' === color, 'External Stylesheet (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('inlinestylediv'),null)['color']; + ok('rgb(0, 255, 0)' === color, 'Inline Style TAG (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('attrstylediv'),null)['color']; + ok('rgb(0, 255, 0)' === color, 'Style Attribute (' + color + ')'); + + // Note that the below test will fail if "script-src: 'unsafe-inline'" breaks, + // since it relies on executing script to set .cssText + color = window.getComputedStyle(cspframe.contentDocument.getElementById('csstextstylediv'),null)['color']; + ok('rgb(0, 255, 0)' === color, 'style.cssText (' + color + ')'); + // SMIL tests + color = window.getComputedStyle(cspframe.contentDocument.getElementById('xmlTest',null))['fill']; + ok('rgb(0, 255, 0)' === color, 'XML Attribute styling (SMIL) (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('cssOverrideTest',null))['fill']; + ok('rgb(0, 255, 0)' === color, 'CSS Override styling (SMIL) (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('cssOverrideTestById',null))['fill']; + ok('rgb(0, 255, 0)' === color, 'CSS Override styling via ID lookup (SMIL) (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('cssSetTestById',null))['fill']; + ok('rgb(0, 255, 0)' === color, 'CSS Set Element styling via ID lookup (SMIL) (' + color + ')'); + + color = window.getComputedStyle(cspframe.contentDocument.getElementById('modifycsstextdiv'),null)['color']; + ok('rgb(0, 255, 0)' === color, 'Modify loaded style sheet via cssText (' + color + ')'); + + checkIfDone(); +} + +function checkIfDone() { + done++; + if (done == 2) + SimpleTest.finish(); +} + +// save this for last so that our listeners are registered. +// ... this loads the testbed of good and bad requests. +document.getElementById('cspframe1').src = 'file_inlinestyle_main.html'; +document.getElementById('cspframe1').addEventListener('load', checkStyles, false); +document.getElementById('cspframe2').src = 'file_inlinestyle_main_allowed.html'; +document.getElementById('cspframe2').addEventListener('load', checkStylesAllowed, false); + +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_invalid_source_expression.html b/dom/security/test/csp/test_invalid_source_expression.html new file mode 100644 index 000000000..ad0b34999 --- /dev/null +++ b/dom/security/test/csp/test_invalid_source_expression.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1086612 - CSP: Let source expression be the empty set in case no valid source can be parsed</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <p id="display"></p> + <div id="content" style="visibility: hidden"> + <iframe style="width:100%;" id="testframe"></iframe> + </div> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +/* Description of the test: + * We try to parse a policy: + * script-src bankid:/* + * where the source expression (bankid:/*) is invalid. In that case the source-expression + * should be the empty set ('none'), see: http://www.w3.org/TR/CSP11/#source-list-parsing + * We confirm that the script is blocked by CSP. + */ + +const policy = "script-src bankid:/*"; + +function runTest() { + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape("tests/dom/security/test/csp/file_invalid_source_expression.html"); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(policy); + + document.getElementById("testframe").addEventListener("load", test, false); + document.getElementById("testframe").src = src; +} + +function test() { + try { + document.getElementById("testframe").removeEventListener('load', test, false); + var testframe = document.getElementById("testframe"); + var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML; + is(divcontent, "blocked", "should be 'blocked'!"); + } + catch (e) { + ok(false, "ERROR: could not access content!"); + } + SimpleTest.finish(); +} + +runTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_leading_wildcard.html b/dom/security/test/csp/test_leading_wildcard.html new file mode 100644 index 000000000..81c9f58ec --- /dev/null +++ b/dom/security/test/csp/test_leading_wildcard.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1032303 - CSP - Keep FULL STOP when matching *.foo.com to disallow loads from foo.com</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <p id="display"></p> + <div id="content" style="visibility: hidden"> + <iframe style="width:100%;" id="testframe"></iframe> + </div> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * We load a page with a CSP that allows scripts to be loaded from *.example.com. + * On that page we try to load two scripts: + * a) [allowed] leading_wildcard_allowed.js which is served from test1.example.com + * b) [blocked] leading_wildcard_blocked.js which is served from example.com + * + * We verify that only the allowed script executes by registering observers which listen + * to CSP violations and http-notifications. Please note that both scripts do *not* exist + * in the file system. The test just verifies that CSP blocks correctly. + */ + +SimpleTest.waitForExplicitFinish(); + +var policy = "default-src 'none' script-src *.example.com"; +var testsExecuted = 0; + +function finishTest() { + if (++testsExecuted < 2) { + return; + } + window.wildCardExaminer.remove(); + SimpleTest.finish(); +} + +// We use the examiner to identify requests that hit the wire and requests +// that are blocked by CSP. +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} +examiner.prototype = { + observe: function(subject, topic, data) { + + // allowed requests + if (topic === "specialpowers-http-notify-request") { + if (data.includes("leading_wildcard_allowed.js")) { + ok (true, "CSP should allow file_leading_wildcard_allowed.js!"); + finishTest(); + } + if (data.includes("leading_wildcard_blocked.js")) { + ok(false, "CSP should not allow file_leading_wildcard_blocked.js!"); + finishTest(); + } + } + + // blocked requests + if (topic === "csp-on-violate-policy") { + var asciiSpec = SpecialPowers.getPrivilegedProps( + SpecialPowers.do_QueryInterface(subject, "nsIURI"), + "asciiSpec"); + + if (asciiSpec.includes("leading_wildcard_allowed.js")) { + ok (false, "CSP should not block file_leading_wildcard_allowed.js!"); + finishTest(); + } + if (asciiSpec.includes("leading_wildcard_blocked.js")) { + ok (true, "CSP should block file_leading_wildcard_blocked.js!"); + finishTest(); + } + } + }, + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} +window.wildCardExaminer = new examiner(); + +function runTest() { + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape("tests/dom/security/test/csp/file_leading_wildcard.html"); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(policy); + + document.getElementById("testframe").src = src; +} + +// start running the tests +runTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_meta_element.html b/dom/security/test/csp/test_meta_element.html new file mode 100644 index 000000000..98c12fce8 --- /dev/null +++ b/dom/security/test/csp/test_meta_element.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 663570 - Implement Content Security Policy via <meta> tag</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<iframe style="width:100%;" id="testframe" src="file_meta_element.html"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * The test is twofold: + * First, by loading a page using meta csp (into an iframe) we make sure that + * images get correctly blocked as the csp policy includes "img-src 'none'"; + * + * Second, we make sure meta csp ignores the following directives: + * * report-uri + * * frame-ancestors + * * sandbox + * + * Please note that the CSP sanbdox directive (bug 671389) has not landed yet. + * Once bug 671389 lands this test will fail and needs to be updated. + */ + +SimpleTest.waitForExplicitFinish(); +const EXPECTED_DIRS = ["img-src", "script-src"]; + +function finishTest() { + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); +} + +function checkResults(result) { + is(result, "img-blocked", "loading images should be blocked by meta csp"); + + try { + // get the csp in JSON notation from the principal + var frame = document.getElementById("testframe"); + var principal = SpecialPowers.wrap(frame.contentDocument).nodePrincipal; + var cspJSON = principal.cspJSON; + ok(cspJSON, "CSP applied through meta element"); + + // parse the cspJSON in a csp-object + var cspOBJ = JSON.parse(cspJSON); + ok(cspOBJ, "was able to parse the JSON"); + + // make sure we only got one policy + var policies = cspOBJ["csp-policies"]; + is(policies.length, 1, "there should be one policy applied"); + + // iterate the policy and make sure to only encounter + // expected directives. + var policy = policies[0]; + for (var dir in policy) { + // special case handling for report-only which is not a directive + // but present in the JSON notation of the CSP. + if (dir === "report-only") { + continue; + } + var index = EXPECTED_DIRS.indexOf(dir); + isnot(index, -1, "meta csp contains directive: " + dir + "!"); + + // take the element out of the array so we can make sure + // that we have seen all the expected values in the end. + EXPECTED_DIRS.splice(index, 1); + } + is(EXPECTED_DIRS.length, 0, "have seen all the expected values"); + } + catch (e) { + ok(false, "uuh, something went wrong within meta csp test"); + } + + finishTest(); +} + +// a postMessage handler used to bubble up the onsuccess/onerror state +// from within the iframe. +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + checkResults(event.data.result); +} + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_meta_header_dual.html b/dom/security/test/csp/test_meta_header_dual.html new file mode 100644 index 000000000..4d6258d6e --- /dev/null +++ b/dom/security/test/csp/test_meta_header_dual.html @@ -0,0 +1,137 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 663570 - Implement Content Security Policy via meta tag</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * We test all sorts of CSPs on documents, including documents with no + * CSP, with meta CSP and with meta CSP in combination with a CSP header. + */ + +const TESTS = [ + { + /* load image without any CSP */ + query: "test1", + result: "img-loaded", + policyLen: 0, + desc: "no CSP should allow load", + }, + { + /* load image where meta denies load */ + query: "test2", + result: "img-blocked", + policyLen: 1, + desc: "meta (img-src 'none') should block load" + }, + { + /* load image where meta allows load */ + query: "test3", + result: "img-loaded", + policyLen: 1, + desc: "meta (img-src http://mochi.test) should allow load" + }, + { + /* load image where meta allows but header blocks */ + query: "test4", // triggers speculative load + result: "img-blocked", + policyLen: 2, + desc: "meta (img-src http://mochi.test), header (img-src 'none') should block load" + }, + { + /* load image where meta blocks but header allows */ + query: "test5", // triggers speculative load + result: "img-blocked", + policyLen: 2, + desc: "meta (img-src 'none'), header (img-src http://mochi.test) should block load" + }, + { + /* load image where meta allows and header allows */ + query: "test6", // triggers speculative load + result: "img-loaded", + policyLen: 2, + desc: "meta (img-src http://mochi.test), header (img-src http://mochi.test) should allow load" + }, + { + /* load image where meta1 allows but meta2 blocks */ + query: "test7", + result: "img-blocked", + policyLen: 2, + desc: "meta1 (img-src http://mochi.test), meta2 (img-src 'none') should allow blocked" + }, + { + /* load image where meta1 allows and meta2 allows */ + query: "test8", + result: "img-loaded", + policyLen: 2, + desc: "meta1 (img-src http://mochi.test), meta2 (img-src http://mochi.test) should allow allowed" + }, +]; + +var curTest; +var counter = -1; + +function finishTest() { + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); +} + +function checkResults(result) { + // make sure the image got loaded or blocked + is(result, curTest.result, curTest.query + ": " + curTest.desc); + + if (curTest.policyLen != 0) { + // make sure that meta policy got not parsed and appended twice + try { + // get the csp in JSON notation from the principal + var frame = document.getElementById("testframe"); + var principal = SpecialPowers.wrap(frame.contentDocument).nodePrincipal; + var cspJSON = principal.cspJSON; + var cspOBJ = JSON.parse(cspJSON); + + // make sure that the speculative policy and the actual policy + // are not appended twice. + var policies = cspOBJ["csp-policies"]; + is(policies.length, curTest.policyLen, curTest.query + " should have: " + curTest.policyLen + " policies"); + } + catch (e) { + ok(false, "uuh, something went wrong within cspToJSON in " + curTest.query); + } + } + // move on to the next test + runNextTest(); +} + +// a postMessage handler used to bubble up the +// onsuccess/onerror state from within the iframe. +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + checkResults(event.data.result); +} + +function runNextTest() { + if (++counter == TESTS.length) { + finishTest(); + return; + } + curTest = TESTS[counter]; + // load next test + document.getElementById("testframe").src = "file_meta_header_dual.sjs?" + curTest.query; +} + +// start the test +SimpleTest.waitForExplicitFinish(); +runNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_meta_whitespace_skipping.html b/dom/security/test/csp/test_meta_whitespace_skipping.html new file mode 100644 index 000000000..67c636c3b --- /dev/null +++ b/dom/security/test/csp/test_meta_whitespace_skipping.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1261634 - Update whitespace skipping for meta csp</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe" src="file_meta_whitespace_skipping.html"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * We load a site using meta CSP into an iframe. We make sure that all directives + * are parsed correclty by the CSP parser even though the directives are separated + * not only by whitespace but also by line breaks + */ + +SimpleTest.waitForExplicitFinish(); +const EXPECTED_DIRS = [ + "img-src", "script-src", "style-src", "child-src", "font-src"]; + +function finishTest() { + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); +} + +function checkResults(result) { + // sanity check that the site was loaded and the meta csp was parsed. + is(result, "meta-csp-parsed", "loading images should be blocked by meta csp"); + + try { + // get the csp in JSON notation from the principal + var frame = document.getElementById("testframe"); + var principal = SpecialPowers.wrap(frame.contentDocument).nodePrincipal; + var cspJSON = principal.cspJSON; + ok(cspJSON, "CSP applied through meta element"); + + // parse the cspJSON in a csp-object + var cspOBJ = JSON.parse(cspJSON); + ok(cspOBJ, "was able to parse the JSON"); + + // make sure we only got one policy + var policies = cspOBJ["csp-policies"]; + is(policies.length, 1, "there should be one policy applied"); + + // iterate the policy and make sure to only encounter + // expected directives. + var policy = policies[0]; + for (var dir in policy) { + // special case handling for report-only which is not a directive + // but present in the JSON notation of the CSP. + if (dir === "report-only") { + continue; + } + var index = EXPECTED_DIRS.indexOf(dir); + isnot(index, -1, "meta csp contains directive: " + dir + "!"); + + // take the element out of the array so we can make sure + // that we have seen all the expected values in the end. + EXPECTED_DIRS.splice(index, 1); + } + is(EXPECTED_DIRS.length, 0, "have seen all the expected values"); + } + catch (e) { + ok(false, "uuh, something went wrong within meta csp test"); + } + + finishTest(); +} + +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + checkResults(event.data.result); +} + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_multi_policy_injection_bypass.html b/dom/security/test/csp/test_multi_policy_injection_bypass.html new file mode 100644 index 000000000..83a9fa265 --- /dev/null +++ b/dom/security/test/csp/test_multi_policy_injection_bypass.html @@ -0,0 +1,119 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=717511 +--> +<head> + <title>Test for Bug 717511</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + + +</div> + +<iframe style="width:200px;height:200px;" id='cspframe'></iframe> +<iframe style="width:200px;height:200px;" id='cspframe2'></iframe> +<script class="testbody" type="text/javascript"> + +var path = "/tests/dom/security/test/csp/"; + +// These are test results: -1 means it hasn't run, +// true/false is the pass/fail result. +// This is not exhaustive, just double-checking the 'self' vs * policy conflict in the two HTTP headers. +window.tests = { + img_good: -1, + img_bad: -1, + script_good: -1, + script_bad: -1, + img2_good: -1, + img2_bad: -1, + script2_good: -1, + script2_bad: -1, +}; + + +// This is used to watch the blocked data bounce off CSP and allowed data +// get sent out to the wire. +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} +examiner.prototype = { + observe: function(subject, topic, data) { + var testpat = new RegExp("testid=([a-z0-9_]+)"); + + //_good things better be allowed! + //_bad things better be stopped! + + if (topic === "specialpowers-http-notify-request") { + //these things were allowed by CSP + var asciiSpec = data; + if (!testpat.test(asciiSpec)) return; + var testid = testpat.exec(asciiSpec)[1]; + window.testResult(testid, + /_good/.test(testid), + asciiSpec + " allowed by csp"); + + } + + if(topic === "csp-on-violate-policy") { + // subject should be an nsIURI for csp-on-violate-policy + if (!SpecialPowers.can_QI(subject)) { + return; + } + + //these were blocked... record that they were blocked + var asciiSpec = SpecialPowers.getPrivilegedProps( + SpecialPowers.do_QueryInterface(subject, "nsIURI"), + "asciiSpec"); + if (!testpat.test(asciiSpec)) return; + var testid = testpat.exec(asciiSpec)[1]; + window.testResult(testid, + /_bad/.test(testid), + asciiSpec + " blocked by \"" + data + "\""); + } + }, + + // must eventually call this to remove the listener, + // or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} + +window.examiner = new examiner(); + +window.testResult = function(testname, result, msg) { + + //test already complete.... forget it... remember the first result. + if (window.tests[testname] != -1) + return; + + window.tests[testname] = result; + is(result, true, testname + ' test: ' + msg); + + // if any test is incomplete, keep waiting + for (var v in window.tests) + if(tests[v] == -1) + return; + + // ... otherwise, finish + window.examiner.remove(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +// save this for last so that our listeners are registered. +// ... this loads the testbed of good and bad requests. +document.getElementById('cspframe').src = 'file_multi_policy_injection_bypass.html'; +document.getElementById('cspframe2').src = 'file_multi_policy_injection_bypass_2.html'; +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_multipartchannel.html b/dom/security/test/csp/test_multipartchannel.html new file mode 100644 index 000000000..3dd42783b --- /dev/null +++ b/dom/security/test/csp/test_multipartchannel.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1223743 - CSP: Check baseChannel for CSP when loading multipart channel</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +/* Description of the test: + * We apply a CSP to a multipart channel and then try to load an image + * within a segment making sure the image is blocked correctly by CSP. + */ + +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + is(event.data, "img-blocked", "image should be blocked"); + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); +} + +// start the test +document.getElementById("testframe").src = "file_multipart_testserver.sjs?doc"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_nonce_source.html b/dom/security/test/csp/test_nonce_source.html new file mode 100644 index 000000000..73a613576 --- /dev/null +++ b/dom/security/test/csp/test_nonce_source.html @@ -0,0 +1,122 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test CSP 1.1 nonce-source for scripts and styles</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="visibility:hidden"> + <iframe style="width:100%;" id='cspframe'></iframe> +</div> +<script class="testbody" type="text/javascript"> + +var testsRun = 0; +var totalTests = 20; + +// This is used to watch the blocked data bounce off CSP +function examiner() { + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); +} + +examiner.prototype = { + observe: function(subject, topic, data) { + var testid_re = new RegExp("testid=([a-z0-9_]+)"); + + //_good things better be allowed! + //_bad things better be blocked! + + if (topic === "specialpowers-http-notify-request") { + var uri = data; + if (!testid_re.test(uri)) return; + var testid = testid_re.exec(uri)[1]; + ok(/_good/.test(testid), "should allow URI with good testid " + testid); + ranTests(1); + } + + if (topic === "csp-on-violate-policy") { + try { + // if it is an blocked external load, subject will be the URI of the resource + var blocked_uri = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec"); + if (!testid_re.test(blocked_uri)) return; + var testid = testid_re.exec(blocked_uri)[1]; + ok(/_bad/.test(testid), "should block URI with bad testid " + testid); + ranTests(1); + } catch (e) { + // if the subject is blocked inline, data will be a violation message + // we can't distinguish which resources triggered these, so we ignore them + } + } + }, + // must eventually call this to remove the listener, or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + } +} + +function cleanup() { + // remove the observer so we don't bork other tests + window.examiner.remove(); + // finish the tests + SimpleTest.finish(); +} + +function ranTests(num) { + testsRun += num; + if (testsRun < totalTests) { + return; + } + cleanup(); +} + +function checkInlineScriptsAndStyles () { + var cspframe = document.getElementById('cspframe'); + var getElementColorById = function (id) { + return window.getComputedStyle(cspframe.contentDocument.getElementById(id), null).color; + }; + // Inline style tries to change an element's color to green. If blocked, the + // element's color will be the (unchanged) default black. + var green = "rgb(0, 128, 0)"; + var red = "rgb(255,0,0)"; + var black = "rgb(0, 0, 0)"; + + // inline script tests + is(getElementColorById('inline-script-correct-nonce'), green, + "Inline script with correct nonce should execute"); + is(getElementColorById('inline-script-incorrect-nonce'), black, + "Inline script with incorrect nonce should not execute"); + is(getElementColorById('inline-script-correct-style-nonce'), black, + "Inline script with correct nonce for styles (but not for scripts) should not execute"); + is(getElementColorById('inline-script-no-nonce'), black, + "Inline script with no nonce should not execute"); + + // inline style tests + is(getElementColorById('inline-style-correct-nonce'), green, + "Inline style with correct nonce should be allowed"); + is(getElementColorById('inline-style-incorrect-nonce'), black, + "Inline style with incorrect nonce should be blocked"); + is(getElementColorById('inline-style-correct-script-nonce'), black, + "Inline style with correct nonce for scripts (but incorrect nonce for styles) should be blocked"); + is(getElementColorById('inline-style-no-nonce'), black, + "Inline style with no nonce should be blocked"); + + ranTests(8); +} + +////////////////////////////////////////////////////////////////////// +// set up and go +window.examiner = new examiner(); +SimpleTest.waitForExplicitFinish(); + +// save this for last so that our listeners are registered. +// ... this loads the testbed of good and bad requests. +document.getElementById('cspframe').src = 'file_nonce_source.html'; +document.getElementById('cspframe').addEventListener('load', checkInlineScriptsAndStyles, false); +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_null_baseuri.html b/dom/security/test/csp/test_null_baseuri.html new file mode 100644 index 000000000..4592d582f --- /dev/null +++ b/dom/security/test/csp/test_null_baseuri.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121857 - document.baseURI should not get blocked if baseURI is null</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <p id="display"></p> + <div id="content" style="visibility: hidden"> + <iframe style="width:100%;" id="testframe"></iframe> + </div> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * Creating a 'base' element and appending that element + * to document.head. After setting baseTag.href and finally + * removing the created element from the head, the baseURI + * should be the inital baseURI of the page. + */ + +const TOTAL_TESTS = 3; +var test_counter = 0; + +// a postMessage handler to communicate the results back to the parent. +window.addEventListener("message", receiveMessage, false); + +function receiveMessage(event) +{ + // make sure the base-uri before and after the test is the initial base uri of the page + if (event.data.test === "initial_base_uri") { + ok(event.data.baseURI.startsWith("http://mochi.test"), "baseURI should be 'http://mochi.test'!"); + } + // check that appending the child and setting the base tag actually affects the base-uri + else if (event.data.test === "changed_base_uri") { + ok(event.data.baseURI === "http://www.base-tag.com/", "baseURI should be 'http://www.base-tag.com'!"); + } + // we shouldn't get here, but just in case, throw an error. + else { + ok(false, "unrecognized test!"); + } + + if (++test_counter === TOTAL_TESTS) { + SimpleTest.finish(); + } +} + +function startTest() { + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape("tests/dom/security/test/csp/file_null_baseuri.html"); + // using 'unsafe-inline' since we load the testcase using an inline script + // within file_null_baseuri.html + src += "&csp=" + escape("default-src * 'unsafe-inline';"); + + document.getElementById("testframe").src = src; +} + + +SimpleTest.waitForExplicitFinish(); +startTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_path_matching.html b/dom/security/test/csp/test_path_matching.html new file mode 100644 index 000000000..e02751803 --- /dev/null +++ b/dom/security/test/csp/test_path_matching.html @@ -0,0 +1,115 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 808292 - Implement path-level host-source matching to CSP</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <p id="display"></p> + <div id="content" style="visibility: hidden"> + <iframe style="width:100%;" id="testframe"></iframe> + </div> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +/* Description of the test: + * We are loading the following url (including a fragment portion): + * http://test1.example.com/tests/dom/security/test/csp/file_path_matching.js#foo + * using different policies and verify that the applied policy is accurately enforced. + */ + +var policies = [ + ["allowed", "*"], + ["allowed", "http://*"], // test for bug 1075230, enforcing scheme and wildcard + ["allowed", "test1.example.com"], + ["allowed", "test1.example.com/"], + ["allowed", "test1.example.com/tests/dom/security/test/csp/"], + ["allowed", "test1.example.com/tests/dom/security/test/csp/file_path_matching.js"], + + ["allowed", "test1.example.com?foo=val"], + ["allowed", "test1.example.com/?foo=val"], + ["allowed", "test1.example.com/tests/dom/security/test/csp/?foo=val"], + ["allowed", "test1.example.com/tests/dom/security/test/csp/file_path_matching.js?foo=val"], + + ["allowed", "test1.example.com#foo"], + ["allowed", "test1.example.com/#foo"], + ["allowed", "test1.example.com/tests/dom/security/test/csp/#foo"], + ["allowed", "test1.example.com/tests/dom/security/test/csp/file_path_matching.js#foo"], + + ["allowed", "*.example.com"], + ["allowed", "*.example.com/"], + ["allowed", "*.example.com/tests/dom/security/test/csp/"], + ["allowed", "*.example.com/tests/dom/security/test/csp/file_path_matching.js"], + + ["allowed", "test1.example.com:80"], + ["allowed", "test1.example.com:80/"], + ["allowed", "test1.example.com:80/tests/dom/security/test/csp/"], + ["allowed", "test1.example.com:80/tests/dom/security/test/csp/file_path_matching.js"], + + ["allowed", "test1.example.com:*"], + ["allowed", "test1.example.com:*/"], + ["allowed", "test1.example.com:*/tests/dom/security/test/csp/"], + ["allowed", "test1.example.com:*/tests/dom/security/test/csp/file_path_matching.js"], + + ["blocked", "test1.example.com/tests"], + ["blocked", "test1.example.com/tests/dom/security/test/csp"], + ["blocked", "test1.example.com/tests/dom/security/test/csp/file_path_matching.py"], + + ["blocked", "test1.example.com:8888/tests"], + ["blocked", "test1.example.com:8888/tests/dom/security/test/csp"], + ["blocked", "test1.example.com:8888/tests/dom/security/test/csp/file_path_matching.py"], + + // case insensitive matching for scheme and host, but case sensitive matching for paths + ["allowed", "HTTP://test1.EXAMPLE.com/tests/"], + ["allowed", "test1.EXAMPLE.com/tests/"], + ["blocked", "test1.example.com/tests/dom/security/test/CSP/?foo=val"], + ["blocked", "test1.example.com/tests/dom/security/test/csp/FILE_path_matching.js?foo=val"], +] + +var counter = 0; +var policy; + +function loadNextTest() { + if (counter == policies.length) { + SimpleTest.finish(); + } + else { + policy = policies[counter++]; + var src = "file_testserver.sjs?file="; + // append the file that should be served + src += (counter % 2 == 0) + // load url including ref: example.com#foo + ? escape("tests/dom/security/test/csp/file_path_matching.html") + // load url including query: example.com?val=foo (bug 1147026) + : escape("tests/dom/security/test/csp/file_path_matching_incl_query.html"); + + // append the CSP that should be used to serve the file + src += "&csp=" + escape("default-src 'none'; script-src " + policy[1]); + + document.getElementById("testframe").addEventListener("load", test, false); + document.getElementById("testframe").src = src; + } +} + +function test() { + try { + document.getElementById("testframe").removeEventListener('load', test, false); + var testframe = document.getElementById("testframe"); + var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML; + is(divcontent, policy[0], "should be " + policy[0] + " in test " + (counter - 1) + "!"); + } + catch (e) { + ok(false, "ERROR: could not access content in test " + (counter - 1) + "!"); + } + loadNextTest(); +} + +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_path_matching_redirect.html b/dom/security/test/csp/test_path_matching_redirect.html new file mode 100644 index 000000000..7d1044052 --- /dev/null +++ b/dom/security/test/csp/test_path_matching_redirect.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 808292 - Implement path-level host-source matching to CSP (redirects)</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <p id="display"></p> + <div id="content" style="visibility: hidden"> + <iframe style="width:100%;" id="testframe"></iframe> + </div> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +/* Description of the test: + * First, we try to load a script where the *path* does not match. + * Second, we try to load a script which is allowed by the CSPs + * script-src directive. The script then gets redirected to + * an URL where the host matches, but the path wouldn't. + * Since 'paths' should not be taken into account after redirects, + * that load should succeed. We are using a similar test setup + * as described in the spec, see: + * http://www.w3.org/TR/CSP11/#source-list-paths-and-redirects + */ + +var policy = "script-src http://example.com http://test1.example.com/CSPAllowsScriptsInThatFolder"; + +var tests = [ + { + // the script in file_path_matching.html + // <script src="http://test1.example.com/tests/dom/security/.."> + // is not within the whitelisted path by the csp-policy + // hence the script is 'blocked' by CSP. + expected: "blocked", + uri: "tests/dom/security/test/csp/file_path_matching.html" + }, + { + // the script in file_path_matching_redirect.html + // <script src="http://example.com/tests/dom/.."> + // gets redirected to: http://test1.example.com/tests/dom + // where after the redirect the path of the policy is not enforced + // anymore and hence execution of the script is 'allowed'. + expected: "allowed", + uri: "tests/dom/security/test/csp/file_path_matching_redirect.html" + }, +]; + +var counter = 0; +var curTest; + +function checkResult() { + try { + document.getElementById("testframe").removeEventListener('load', checkResult, false); + var testframe = document.getElementById("testframe"); + var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML; + is(divcontent, curTest.expected, "should be blocked in test " + (counter - 1) + "!"); + } + catch (e) { + ok(false, "ERROR: could not access content in test " + (counter - 1) + "!"); + } + loadNextTest(); +} + +function loadNextTest() { + if (counter == tests.length) { + SimpleTest.finish(); + } + else { + curTest = tests[counter++]; + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape(curTest.uri); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(policy); + + document.getElementById("testframe").addEventListener("load", checkResult, false); + document.getElementById("testframe").src = src; + } +} + +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_ping.html b/dom/security/test/csp/test_ping.html new file mode 100644 index 000000000..a896794e1 --- /dev/null +++ b/dom/security/test/csp/test_ping.html @@ -0,0 +1,103 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1100181 - CSP: Enforce connect-src when submitting pings</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * We load a page with a given CSP and verify that hyperlink auditing + * is correctly evaluated through the "connect-src" directive. + */ + +// Need to pref hyperlink auditing on since it's disabled by default. +SpecialPowers.setBoolPref("browser.send_pings", true); + +SimpleTest.waitForExplicitFinish(); + +var tests = [ + { + result : "allowed", + policy : "connect-src 'self'" + }, + { + result : "blocked", + policy : "connect-src 'none'" + } +]; + +// initializing to -1 so we start at index 0 when we start the test +var counter = -1; + +function checkResult(aResult) { + is(aResult, tests[counter].result, "should be " + tests[counter].result + " in test " + counter + "!"); + loadNextTest(); +} + +// We use the examiner to identify requests that hit the wire and requests +// that are blocked by CSP and bubble up the result to the including iframe +// document (parent). +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} +examiner.prototype = { + observe: function(subject, topic, data) { + if (topic === "specialpowers-http-notify-request") { + // making sure we do not bubble a result for something + // other then the request in question. + if (!data.includes("send-ping")) { + return; + } + checkResult("allowed"); + return; + } + + if (topic === "csp-on-violate-policy") { + // making sure we do not bubble a result for something + // other then the request in question. + var asciiSpec = SpecialPowers.getPrivilegedProps( + SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec"); + if (!asciiSpec.includes("send-ping")) { + return; + } + checkResult("blocked"); + } + }, + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} +window.ConnectSrcExaminer = new examiner(); + +function loadNextTest() { + counter++; + if (counter == tests.length) { + window.ConnectSrcExaminer.remove(); + SimpleTest.finish(); + return; + } + + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape("tests/dom/security/test/csp/file_ping.html"); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(tests[counter].policy); + + document.getElementById("testframe").src = src; +} + +// start running the tests +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_policyuri_regression_from_multipolicy.html b/dom/security/test/csp/test_policyuri_regression_from_multipolicy.html new file mode 100644 index 000000000..f69e8f558 --- /dev/null +++ b/dom/security/test/csp/test_policyuri_regression_from_multipolicy.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 924708</title> + <!-- + test that a report-only policy that uses policy-uri is not incorrectly + enforced due to regressions introduced by Bug 836922. + --> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:200px;height:200px;" id='testframe'></iframe> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var testframe = document.getElementById('testframe'); +testframe.src = 'file_policyuri_regression_from_multipolicy.html'; +testframe.addEventListener('load', function checkInlineScriptExecuted () { + is(this.contentDocument.getElementById('testdiv').innerHTML, + 'Inline Script Executed', + 'Inline script should execute (it would be blocked by the policy, but the policy is report-only)'); + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_redirects.html b/dom/security/test/csp/test_redirects.html new file mode 100644 index 000000000..df01e3b41 --- /dev/null +++ b/dom/security/test/csp/test_redirects.html @@ -0,0 +1,137 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Tests for Content Security Policy during redirects</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> + +<iframe style="width:100%;height:300px;" id="harness"></iframe> +<pre id="log"></pre> +<script class="testbody" type="text/javascript"> + +var path = "/tests/dom/security/test/csp/"; + +// debugging +function log(s) { + return; + dump("**" + s + "\n"); + var log = document.getElementById("log"); + log.textContent = log.textContent+s+"\n"; +} + +// used to watch if requests are blocked by CSP or allowed through +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} +examiner.prototype = { + observe: function(subject, topic, data) { + var testpat = new RegExp("testid=([a-z0-9-]+)"); + var asciiSpec; + var testid; + + if (topic === "specialpowers-http-notify-request") { + // request was sent + var allowedUri = data; + if (!testpat.test(allowedUri)) return; + testid = testpat.exec(allowedUri)[1]; + if (testExpectedResults[testid] == "completed") return; + log("allowed: "+allowedUri); + window.testResult(testid, allowedUri, true); + } + + else if (topic === "csp-on-violate-policy") { + // request was blocked + asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec"); + if (!testpat.test(asciiSpec)) return; + testid = testpat.exec(asciiSpec)[1]; + // had to add this check because http-on-modify-request can fire after + // csp-on-violate-policy, apparently, even though the request does + // not hit the wire. + if (testExpectedResults[testid] == "completed") return; + log("BLOCKED: "+asciiSpec); + window.testResult(testid, asciiSpec, false); + } + }, + + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} +window.examiner = new examiner(); + +// contains { test_frame_id : expected_result } +var testExpectedResults = { "font-src": true, + "font-src-redir": false, + "frame-src": true, + "frame-src-redir": false, + "img-src": true, + "img-src-redir": false, + "media-src": true, + "media-src-redir": false, + "object-src": true, + "object-src-redir": false, + "script-src": true, + "script-src-redir": false, + "style-src": true, + "style-src-redir": false, + "xhr-src": true, + "xhr-src-redir": false, + "from-worker": true, + "script-src-redir-from-worker": true, // redir is allowed since policy isn't inherited + "xhr-src-redir-from-worker": true, // redir is allowed since policy isn't inherited + "fetch-src-redir-from-worker": true, // redir is allowed since policy isn't inherited + "from-blob-worker": true, + "script-src-redir-from-blob-worker": false, + "xhr-src-redir-from-blob-worker": false, + "fetch-src-redir-from-blob-worker": false, + "img-src-from-css": true, + "img-src-redir-from-css": false, + }; + +// takes the name of the test, the URL that was tested, and whether the +// load occurred +var testResult = function(testName, url, result) { + log(" testName: "+testName+", result: "+result+", expected: "+testExpectedResults[testName]+"\n"); + is(result, testExpectedResults[testName], testName+" test: "+url); + + // mark test as completed + testExpectedResults[testName] = "completed"; + + // don't finish until we've run all the tests + for (var t in testExpectedResults) { + if (testExpectedResults[t] != "completed") { + return; + } + } + + window.examiner.remove(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.pushPrefEnv( + {'set':[// This defaults to 0 ("preload none") on mobile (B2G/Android), which + // blocks loading the resource until the user interacts with a + // corresponding widget, which breaks the media_* tests. We set it + // back to the default used by desktop Firefox to get consistent + // behavior. + ["media.preload.default", 2]]}, + function() { + // save this for last so that our listeners are registered. + // ... this loads the testbed of good and bad requests. + document.getElementById("harness").src = "file_redirects_main.html"; + }); +</script> +</pre> + +</body> +</html> diff --git a/dom/security/test/csp/test_referrerdirective.html b/dom/security/test/csp/test_referrerdirective.html new file mode 100644 index 000000000..770fcc40b --- /dev/null +++ b/dom/security/test/csp/test_referrerdirective.html @@ -0,0 +1,145 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=965727 +--> +<head> + <meta charset="utf-8"> + <title>Test for Content Security Policy referrer Directive (Bug 965727)</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> +/* + * This tests various referrer policies and the referrer-sending behavior when + * requesting scripts in different ways: + * - cross-origin (https://example.com -> https://test2.example.com) + * - same-origin (https://example.com -> https://example.com) + * - downgrade (https://example.com -> http://example.com) + * + * Each test creates an iframe that loads scripts for each of the checks. If + * the scripts are blocked, the test fails (they should run). When loaded, + * each script updates a results object in the test page, and then when the + * test page has finished loading all the scripts, it postMessages back to this + * page. Once all tests are done, the results are checked. + */ + +var testData = { + 'default': { 'csp': "script-src * 'unsafe-inline'; referrer default", + 'expected': { 'sameorigin': 'full', + 'crossorigin': 'full', + 'downgrade': 'none' }}, + + 'origin': { 'csp': "script-src * 'unsafe-inline'; referrer origin", + 'expected': { 'sameorigin': 'origin', + 'crossorigin': 'origin', + 'downgrade': 'origin' }}, + + 'origin-when-cross-origin': { 'csp': "script-src * 'unsafe-inline'; referrer origin-when-cross-origin", + 'expected': { 'sameorigin': 'full', + 'crossorigin': 'origin', + 'downgrade': 'origin' }}, + + 'unsafe-url': { 'csp': "script-src * 'unsafe-inline'; referrer unsafe-url", + 'expected': { 'sameorigin': 'full', + 'crossorigin': 'full', + 'downgrade': 'full' }}, + + 'none': { 'csp': "script-src * 'unsafe-inline'; referrer no-referrer", + 'expected': { 'sameorigin': 'none', + 'crossorigin': 'none', + 'downgrade': 'none' }}, + + // referrer delivered through CSPRO should be ignored + 'ignore-cspro': { 'cspro': "script-src * 'unsafe-inline'; referrer origin", + 'expected': { 'sameorigin': 'full', + 'crossorigin': 'full', + 'downgrade': 'none' }}, + + // referrer delivered through CSPRO should be ignored + 'ignore-cspro2': { 'csp' : "script-src * 'unsafe-inline'; referrer no-referrer", + 'cspro': "script-src * 'unsafe-inline'; referrer origin", + 'expected': { 'sameorigin': 'none', + 'crossorigin': 'none', + 'downgrade': 'none' }}, + }; + +var referrerDirectiveTests = { + // called via postMessage when one of the iframes is done running. + onIframeComplete: function(event) { + try { + var results = JSON.parse(event.data); + ok(results.hasOwnProperty('id'), "'id' property required in posted message " + event.data); + + ok(testData.hasOwnProperty(results['id']), "Test " + results['id'] + " must be expected."); + + // check all the various load types' referrers. + var expected = testData[results['id']].expected; + for (var t in expected) { + is(results.results[t], expected[t], + " referrer must match expected for " + t + " in " + results['id']); + } + testData[results['id']]['complete'] = true; + + } catch(e) { + // fail -- should always be JSON + ok(false, "failed to parse posted message + " + event.data); + // have to end as well since not all messages were valid. + SimpleTest.finish(); + } + + referrerDirectiveTests.checkForCompletion(); + }, + + // checks to see if all the parallel tests are done and validates results. + checkForCompletion: function() { + for (var id in testData) { + if (!testData[id].hasOwnProperty('complete')) { + return; + } + } + SimpleTest.finish(); + } +}; + +SimpleTest.waitForExplicitFinish(); +// have to disable mixed content blocking to test https->http referrers. +SpecialPowers.pushPrefEnv({ + 'set': [['security.mixed_content.block_active_content', false], + ['security.mixed_content.block_display_content', false], + ['security.mixed_content.send_hsts_priming', false], + ['security.mixed_content.use_hsts', false], + ] + }, + function() { + // each of the iframes we create will call us back when its contents are loaded. + window.addEventListener("message", referrerDirectiveTests.onIframeComplete.bind(window), false); + + // one iframe created for each test case + for (var id in testData) { + var elt = document.createElement("iframe"); + var src = "https://example.com/tests/dom/security/test/csp/file_testserver.sjs?id=" + id; + if (testData[id]['csp']) { + src += "&csp=" + escape(testData[id]['csp']); + } + if (testData[id]['cspro']) { + src += "&cspro=" + escape(testData[id]['cspro']); + } + src += "&file=tests/dom/security/test/csp/file_referrerdirective.html"; + elt.src = src; + document.getElementById("content").appendChild(elt); + } + }); +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_report.html b/dom/security/test/csp/test_report.html new file mode 100644 index 000000000..e8cfb8778 --- /dev/null +++ b/dom/security/test/csp/test_report.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=548193 +--> +<head> + <title>Test for Bug 548193</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<iframe style="width:200px;height:200px;" id='cspframe'></iframe> +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * We try to load an inline-src using a policy that constrains + * all scripts from running (default-src 'none'). We verify that + * the generated csp-report contains the expceted values. If any + * of the JSON is not formatted properly (e.g. not properly escaped) + * then JSON.parse will fail, which allows to pinpoint such errors + * in the catch block, and the test will fail. Since we use an + * observer, we can set the actual report-uri to a foo value. + */ + +const testfile = "tests/dom/security/test/csp/file_report.html"; +const reportURI = "http://mochi.test:8888/foo.sjs"; +const policy = "default-src 'none'; report-uri " + reportURI; +const docUri = "http://mochi.test:8888/tests/dom/security/test/csp/file_testserver.sjs" + + "?file=tests/dom/security/test/csp/file_report.html" + + "&csp=default-src%20%27none%27%3B%20report-uri%20http%3A//mochi.test%3A8888/foo.sjs"; + +window.checkResults = function(reportObj) { + var cspReport = reportObj["csp-report"]; + + // The following uris' fragments should be stripped before reporting: + // * document-uri + // * blocked-uri + // * source-file + // see http://www.w3.org/TR/CSP11/#violation-reports + is(cspReport["document-uri"], docUri, "Incorrect document-uri"); + + // we can not test for the whole referrer since it includes platform specific information + ok(cspReport["referrer"].startsWith("http://mochi.test:8888/tests/dom/security/test/csp/test_report.html"), + "Incorrect referrer"); + + is(cspReport["blocked-uri"], "self", "Incorrect blocked-uri"); + + is(cspReport["violated-directive"], "default-src 'none'", "Incorrect violated-directive"); + + is(cspReport["original-policy"], "default-src 'none'; report-uri http://mochi.test:8888/foo.sjs", + "Incorrect original-policy"); + + is(cspReport["source-file"], docUri, "Incorrect source-file"); + + is(cspReport["script-sample"], "\n var foo = \"propEscFoo\";\n var bar...", + "Incorrect script-sample"); + + is(cspReport["line-number"], 7, "Incorrect line-number"); +} + +var chromeScriptUrl = SimpleTest.getTestFileURL("file_report_chromescript.js"); +var script = SpecialPowers.loadChromeScript(chromeScriptUrl); + +script.addMessageListener('opening-request-completed', function ml(msg) { + if (msg.error) { + ok(false, "Could not query report (exception: " + msg.error + ")"); + } else { + try { + var reportObj = JSON.parse(msg.report); + } catch (e) { + ok(false, "Could not parse JSON (exception: " + e + ")"); + } + try { + // test for the proper values in the report object + window.checkResults(reportObj); + } catch (e) { + ok(false, "Could not query report (exception: " + e + ")"); + } + } + + script.removeMessageListener('opening-request-completed', ml); + SimpleTest.finish(); +}); + +SimpleTest.waitForExplicitFinish(); + +// load the resource which will generate a CSP violation report +// save this for last so that our listeners are registered. +var src = "file_testserver.sjs"; +// append the file that should be served +src += "?file=" + escape(testfile); +// append the CSP that should be used to serve the file +src += "&csp=" + escape(policy); +// appending a fragment so we can test that it's correctly stripped +// for document-uri and source-file. +src += "#foo"; +document.getElementById("cspframe").src = src; + +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_report_for_import.html b/dom/security/test/csp/test_report_for_import.html new file mode 100644 index 000000000..be112d51c --- /dev/null +++ b/dom/security/test/csp/test_report_for_import.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=548193 +--> +<head> + <title>Test for Bug 548193</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<iframe style="width:200px;height:200px;" id='cspframe'></iframe> +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * We are loading a stylesheet using a csp policy that only allows styles from 'self' + * to be loaded. In other words, the *.css file itself should be allowed to load, but + * the @import file within the CSS should get blocked. We verify that the generated + * csp-report is sent and contains all the expected values. + * In detail, the test starts by sending an XHR request to the report-server + * which waits on the server side till the report was received and hands the + * report in JSON format back to the testfile which then verifies accuracy + * of all the different report fields in the CSP report. + */ + +const TEST_FILE = "tests/dom/security/test/csp/file_report_for_import.html"; +const REPORT_URI = + "http://mochi.test:8888/tests/dom/security/test/csp/file_report_for_import_server.sjs?report"; +const POLICY = "style-src 'self'; report-uri " + REPORT_URI; + +const DOC_URI = + "http://mochi.test:8888/tests/dom/security/test/csp/file_testserver.sjs?" + + "file=tests/dom/security/test/csp/file_report_for_import.html&" + + "csp=style-src%20%27self%27%3B%20" + + "report-uri%20http%3A//mochi.test%3A8888/tests/dom/security/test/csp/" + + "file_report_for_import_server.sjs%3Freport"; + +function checkResults(reportStr) { + try { + var reportObj = JSON.parse(reportStr); + var cspReport = reportObj["csp-report"]; + + is(cspReport["document-uri"], DOC_URI, "Incorrect document-uri"); + is(cspReport["referrer"], + "http://mochi.test:8888/tests/dom/security/test/csp/test_report_for_import.html", + "Incorrect referrer"); + is(cspReport["violated-directive"], + "style-src http://mochi.test:8888", + "Incorrect violated-directive"); + is(cspReport["original-policy"], + "style-src http://mochi.test:8888; report-uri " + + "http://mochi.test:8888/tests/dom/security/test/csp/file_report_for_import_server.sjs?report", + "Incorrect original-policy"); + is(cspReport["blocked-uri"], + "http://example.com", + "Incorrect blocked-uri"); + + // we do not always set the following fields + is(cspReport["source-file"], undefined, "Incorrect source-file"); + is(cspReport["script-sample"], undefined, "Incorrect script-sample"); + is(cspReport["line-number"], undefined, "Incorrect line-number"); + } + catch (e) { + ok(false, "Could not parse JSON (exception: " + e + ")"); + } +} + +function loadTestPageIntoFrame() { + // load the resource which will generate a CSP violation report + // save this for last so that our listeners are registered. + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape(TEST_FILE); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(POLICY); + // appending a fragment so we can test that it's correctly stripped + // for document-uri and source-file. + src += "#foo"; + document.getElementById("cspframe").src = src; +} + +function runTest() { + // send an xhr request to the server which is processed async, which only + // returns after the server has received the csp report. + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "file_report_for_import_server.sjs?queryresult"); + myXHR.onload = function(e) { + checkResults(myXHR.responseText); + SimpleTest.finish(); + } + myXHR.onerror = function(e) { + ok(false, "could not query results from server (" + e.message + ")"); + SimpleTest.finish(); + } + myXHR.send(); + + // give it some time and run the testpage + SimpleTest.executeSoon(loadTestPageIntoFrame); +} + +SimpleTest.waitForExplicitFinish(); +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_report_uri_missing_in_report_only_header.html b/dom/security/test/csp/test_report_uri_missing_in_report_only_header.html new file mode 100644 index 000000000..d02911141 --- /dev/null +++ b/dom/security/test/csp/test_report_uri_missing_in_report_only_header.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=847081 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 847081</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=847081">Mozilla Bug 847081</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<iframe id="cspframe"></iframe> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +var stringBundleService = SpecialPowers.Cc["@mozilla.org/intl/stringbundle;1"] + .getService(SpecialPowers.Ci.nsIStringBundleService); +var localizer = stringBundleService.createBundle("chrome://global/locale/security/csp.properties"); +var warningMsg = localizer.formatStringFromName("reportURInotInReportOnlyHeader", [window.location.origin], 1); +function cleanup() { + SpecialPowers.postConsoleSentinel(); + SimpleTest.finish(); +} + +SpecialPowers.registerConsoleListener(function ConsoleMsgListener(aMsg) { + if (aMsg.message.indexOf(warningMsg) > -1) { + ok(true, "report-uri not specified in Report-Only should throw a CSP warning."); + SimpleTest.executeSoon(cleanup); + return; + } else { + // if some other console message is present, we wait + return; + } +}); + + +// set up and start testing +SimpleTest.waitForExplicitFinish(); +document.getElementById('cspframe').src = 'file_report_uri_missing_in_report_only_header.html'; +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_require_sri_meta.html b/dom/security/test/csp/test_require_sri_meta.html new file mode 100644 index 000000000..a06fe122a --- /dev/null +++ b/dom/security/test/csp/test_require_sri_meta.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1277557 - CSP require-sri-for does not block when CSP is in meta tag</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * We load scripts within an iframe and make sure that meta-csp of + * require-sri-for applies correctly to preloaded scripts. + * Please note that we have to use <script src=""> to kick + * off the html preloader. + */ + +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.setBoolPref("security.csp.experimentalEnabled", true); + +var curTest; +var counter = -1; + +const tests = [ + { // test 1 + description: "script with *no* SRI should be blocked", + query: "no-sri", + expected: "script-blocked" + }, + { // test 2 + description: "script-with *incorrect* SRI should be blocked", + query: "wrong-sri", + expected: "script-blocked" + }, + { // test 3 + description: "script-with *correct* SRI should be loaded", + query: "correct-sri", + expected: "script-loaded" + }, +]; + +function finishTest() { + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); +} + +function checkResults(result) { + is(result, curTest.expected, curTest.description); + loadNextTest(); +} + +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + checkResults(event.data.result); +} + +function loadNextTest() { + counter++; + if (counter == tests.length) { + finishTest(); + return; + } + curTest = tests[counter]; + var testframe = document.getElementById("testframe"); + testframe.src = "file_require_sri_meta.sjs?" + curTest.query; +} + +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_sandbox.html b/dom/security/test/csp/test_sandbox.html new file mode 100644 index 000000000..f0df66d6a --- /dev/null +++ b/dom/security/test/csp/test_sandbox.html @@ -0,0 +1,249 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests for bugs 886164 and 671389</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +</div> + +<script class="testbody" type="text/javascript"> + +var testCases = [ + { + // Test 1: don't load image from non-same-origin; allow loading + // images from same-same origin + sandboxAttribute: "allow-same-origin", + csp: "default-src 'self'", + file: "file_sandbox_1.html", + results: { img1a_good: -1, img1_bad: -1 } + // fails if scripts execute + }, + { + // Test 2: don't load image from non-same-origin; allow loading + // images from same-same origin, even without allow-same-origin + // flag + sandboxAttribute: "", + csp: "default-src 'self'", + file: "file_sandbox_2.html", + results: { img2_bad: -1, img2a_good: -1 } + // fails if scripts execute + }, + { + // Test 3: disallow loading images from any host, even with + // allow-same-origin flag set + sandboxAttribute: "allow-same-origin", + csp: "default-src 'none'", + file: "file_sandbox_3.html", + results: { img3_bad: -1, img3a_bad: -1 }, + // fails if scripts execute + }, + { + // Test 4: disallow loading images from any host + sandboxAttribute: "", + csp: "default-src 'none'", + file: "file_sandbox_4.html", + results: { img4_bad: -1, img4a_bad: -1 } + // fails if scripts execute + }, + { + // Test 5: disallow loading images or scripts, allow inline scripts + sandboxAttribute: "allow-scripts", + csp: "default-src 'none'; script-src 'unsafe-inline';", + file: "file_sandbox_5.html", + results: { img5_bad: -1, img5a_bad: -1, script5_bad: -1, script5a_bad: -1 }, + nrOKmessages: 2 // sends 2 ok message + // fails if scripts execute + }, + { + // Test 6: disallow non-same-origin images, allow inline and same origin scripts + sandboxAttribute: "allow-same-origin allow-scripts", + csp: "default-src 'self' 'unsafe-inline';", + file: "file_sandbox_6.html", + results: { img6_bad: -1, script6_bad: -1 }, + nrOKmessages: 4 // sends 4 ok message + // fails if forms are not disallowed + }, + { + // Test 7: same as Test 1 + csp: "default-src 'self'; sandbox allow-same-origin", + file: "file_sandbox_7.html", + results: { img7a_good: -1, img7_bad: -1 } + }, + { + // Test 8: same as Test 2 + csp: "sandbox allow-same-origin; default-src 'self'", + file: "file_sandbox_8.html", + results: { img8_bad: -1, img8a_good: -1 } + }, + { + // Test 9: same as Test 3 + csp: "default-src 'none'; sandbox allow-same-origin", + file: "file_sandbox_9.html", + results: { img9_bad: -1, img9a_bad: -1 } + }, + { + // Test 10: same as Test 4 + csp: "default-src 'none'; sandbox allow-same-origin", + file: "file_sandbox_10.html", + results: { img10_bad: -1, img10a_bad: -1 } + }, + { + // Test 11: same as Test 5 + csp: "default-src 'none'; script-src 'unsafe-inline'; sandbox allow-scripts allow-same-origin", + file: "file_sandbox_11.html", + results: { img11_bad: -1, img11a_bad: -1, script11_bad: -1, script11a_bad: -1 }, + nrOKmessages: 2 // sends 2 ok message + }, + { + // Test 12: same as Test 6 + csp: "sandbox allow-same-origin allow-scripts; default-src 'self' 'unsafe-inline';", + file: "file_sandbox_12.html", + results: { img12_bad: -1, script12_bad: -1 }, + nrOKmessages: 4 // sends 4 ok message + }, + { + // Test 13: same as Test 5 and Test 11, but: + // * using sandbox flag 'allow-scripts' in CSP and not as iframe attribute + // * not using allow-same-origin in CSP (so a new NullPrincipal is created). + csp: "default-src 'none'; script-src 'unsafe-inline'; sandbox allow-scripts", + file: "file_sandbox_13.html", + results: { img13_bad: -1, img13a_bad: -1, script13_bad: -1, script13a_bad: -1 }, + nrOKmessages: 2 // sends 2 ok message + }, +]; + +// a postMessage handler that is used by sandboxed iframes without +// 'allow-same-origin' to communicate pass/fail back to this main page. +// it expects to be called with an object like: +// { ok: true/false, +// desc: <description of the test> which it then forwards to ok() } +window.addEventListener("message", receiveMessage, false); + +function receiveMessage(event) { + ok_wrapper(event.data.ok, event.data.desc); +} + +var completedTests = 0; +var passedTests = 0; + +var totalTests = (function() { + var nrCSPloadTests = 0; + for(var i = 0; i < testCases.length; i++) { + nrCSPloadTests += Object.keys(testCases[i].results).length; + if (testCases[i].nrOKmessages) { + // + number of expected postMessages from iframe + nrCSPloadTests += testCases[i].nrOKmessages; + } + } + return nrCSPloadTests; +})(); + +function ok_wrapper(result, desc) { + ok(result, desc); + + completedTests++; + + if (result) { + passedTests++; + } + + if (completedTests === totalTests) { + window.examiner.remove(); + SimpleTest.finish(); + } +} + +// Set the iframe src and sandbox attribute +function runTest(test) { + var iframe = document.createElement('iframe'); + + document.getElementById('content').appendChild(iframe); + + // set sandbox attribute + if (test.sandboxAttribute !== undefined) { + iframe.sandbox = test.sandboxAttribute; + } + + // set query string + var src = 'file_testserver.sjs'; + // path where the files are + var path = '/tests/dom/security/test/csp/'; + + src += '?file=' + escape(path+test.file); + + if (test.csp !== undefined) { + src += '&csp=' + escape(test.csp); + } + + iframe.src = src; + iframe.width = iframe.height = 10; +} + +// Examiner related + +// This is used to watch the blocked data bounce off CSP and allowed data +// get sent out to the wire. +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request", false); +} + +examiner.prototype = { + observe: function(subject, topic, data) { + var testpat = new RegExp("testid=([a-z0-9_]+)"); + + //_good things better be allowed! + //_bad things better be stopped! + + if (topic === "specialpowers-http-notify-request") { + //these things were allowed by CSP + var uri = data; + if (!testpat.test(uri)) return; + var testid = testpat.exec(uri)[1]; + + if(/_good/.test(testid)) { + ok_wrapper(true, uri + " is allowed by csp"); + } else { + ok_wrapper(false, uri + " should not be allowed by csp"); + } + } + + if(topic === "csp-on-violate-policy") { + //these were blocked... record that they were blocked + var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec"); + if (!testpat.test(asciiSpec)) return; + var testid = testpat.exec(asciiSpec)[1]; + if(/_bad/.test(testid)) { + ok_wrapper(true, asciiSpec + " was blocked by \"" + data + "\""); + } else { + ok_wrapper(false, asciiSpec + " should have been blocked by \"" + data + "\""); + } + } + }, + + // must eventually call this to remove the listener, + // or mochitests might get borked. + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} + +window.examiner = new examiner(); + +SimpleTest.waitForExplicitFinish(); + +(function() { // Run tests: + for(var i = 0; i < testCases.length; i++) { + runTest(testCases[i]); + } +})(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_sandbox_allow_scripts.html b/dom/security/test/csp/test_sandbox_allow_scripts.html new file mode 100644 index 000000000..10acaae43 --- /dev/null +++ b/dom/security/test/csp/test_sandbox_allow_scripts.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1396320: Fix CSP sandbox regression for allow-scripts</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * Load an iframe using a CSP of 'sandbox allow-scripts' and make sure + * the security context of the iframe is sandboxed (cross origin) + */ +SimpleTest.waitForExplicitFinish(); + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + is(event.data.result, "", + "document.domain of sandboxed iframe should be opaque"); + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +let testframe = document.getElementById("testframe"); +testframe.src = "file_sandbox_allow_scripts.html"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_scheme_relative_sources.html b/dom/security/test/csp/test_scheme_relative_sources.html new file mode 100644 index 000000000..21271cdd1 --- /dev/null +++ b/dom/security/test/csp/test_scheme_relative_sources.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 921493 - CSP: test whitelisting of scheme-relative sources</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +/* Description of the test: + * We load http and https pages and verify that scheme relative sources + * are allowed unless its a downgrade from https -> http. + * + * Please note that the policy contains 'unsafe-inline' so we can use + * an inline script to query the result from within the sandboxed iframe + * and report it back to the parent document. + */ + +var POLICY = "default-src 'none'; script-src 'unsafe-inline' example.com;"; + +var tests = [ + { + description: "http -> http", + from: "http", + to: "http", + result: "allowed", + }, + { + description: "http -> https", + from: "http", + to: "https", + result: "allowed", + }, + { + description: "https -> https", + from: "https", + to: "https", + result: "allowed", + }, + { + description: "https -> http", + from: "https", + to: "http", + result: "blocked", + } +]; + +var counter = 0; +var curTest; + +function loadNextTest() { + if (counter == tests.length) { + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); + return; + } + + curTest = tests[counter++]; + + var src = curTest.from + + "://example.com/tests/dom/security/test/csp/file_scheme_relative_sources.sjs" + + "?scheme=" + curTest.to + + "&policy=" + escape(POLICY); + + document.getElementById("testframe").src = src; +} + +// using a postMessage handler to report the result back from +// within the sandboxed iframe without 'allow-same-origin'. +window.addEventListener("message", receiveMessage, false); + +function receiveMessage(event) { + + is(event.data.result, curTest.result, + "should be " + curTest.result + " in test (" + curTest.description + ")!"); + + loadNextTest(); +} + +// get the test started +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_self_none_as_hostname_confusion.html b/dom/security/test/csp/test_self_none_as_hostname_confusion.html new file mode 100644 index 000000000..e5b93c538 --- /dev/null +++ b/dom/security/test/csp/test_self_none_as_hostname_confusion.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=587377 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 587377</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=587377">Mozilla Bug 587377</a> +<p id="display"></p> + +<iframe id="cspframe"></iframe> + +<pre id="test"> + +<script class="testbody" type="text/javascript"> +// Load locale string during mochitest +var stringBundleService = SpecialPowers.Cc["@mozilla.org/intl/stringbundle;1"] + .getService(SpecialPowers.Ci.nsIStringBundleService); +var localizer = stringBundleService.createBundle("chrome://global/locale/security/csp.properties"); +var confusionMsg = localizer.formatStringFromName("hostNameMightBeKeyword", ["SELF", "self"], 2); + +function cleanup() { + SpecialPowers.postConsoleSentinel(); + SimpleTest.finish(); +}; + +// To prevent the test from asserting twice and calling SimpleTest.finish() twice, +// startTest will be marked false as soon as the confusionMsg is detected. +startTest = false; +SpecialPowers.registerConsoleListener(function ConsoleMsgListener(aMsg) { + if (startTest) { + if (aMsg.message.indexOf(confusionMsg) > -1) { + startTest = false; + ok(true, "CSP header with a hostname similar to keyword should be warned"); + SimpleTest.executeSoon(cleanup); + } else { + // don't see the warning yet? wait. + return; + } + } +}); + +// set up and start testing +SimpleTest.waitForExplicitFinish(); +document.getElementById('cspframe').src = 'file_self_none_as_hostname_confusion.html'; +startTest = true; +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_sendbeacon.html b/dom/security/test/csp/test_sendbeacon.html new file mode 100644 index 000000000..1b4cfbc86 --- /dev/null +++ b/dom/security/test/csp/test_sendbeacon.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1234813 - sendBeacon should not throw if blocked by Content Policy</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<iframe style="width:100%;" id="testframe" src="file_sendbeacon.html"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * Let's try to fire a sendBeacon which gets blocked by CSP. Let's make sure + * sendBeacon does not throw an exception. + */ +SimpleTest.waitForExplicitFinish(); + +// a postMessage handler used to bubble up the +// result from within the iframe. +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + var result = event.data.result; + is(result, "blocked-beacon-does-not-throw", "sendBeacon should not throw"); + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); +} + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_service_worker.html b/dom/security/test/csp/test_service_worker.html new file mode 100644 index 000000000..0cff84751 --- /dev/null +++ b/dom/security/test/csp/test_service_worker.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1208559 - ServiceWorker registration not governed by CSP</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * Spawning a worker from https://example.com but script-src is 'test1.example.com' + * CSP is not consulted + */ +SimpleTest.waitForExplicitFinish(); + +var tests = [ + { + policy: "default-src 'self'; script-src 'unsafe-inline'; child-src test1.example.com;", + expected: "blocked" + }, +]; + +var counter = 0; +var curTest; + +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + is(event.data.result, curTest.expected, "Should be (" + curTest.expected + ") in Test " + counter + "!"); + loadNextTest(); +} + +onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true] + ]}, loadNextTest); +} + +function loadNextTest() { + if (counter == tests.length) { + SimpleTest.finish(); + return; + } + curTest = tests[counter++]; + var src = "https://example.com/tests/dom/security/test/csp/file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape("tests/dom/security/test/csp/file_service_worker.html"); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(curTest.policy); + document.getElementById("testframe").src = src; +} + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_shouldprocess.html b/dom/security/test/csp/test_shouldprocess.html new file mode 100644 index 000000000..5d0925167 --- /dev/null +++ b/dom/security/test/csp/test_shouldprocess.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=908933 +--> +<head> + <title>Test Bug 908933</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> +</head> +<body> +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * We load variations of 'objects' and make sure all the + * resource loads are correctly blocked by CSP. + * For all the testing we use a CSP with "object-src 'none'" + * so that all the loads are either blocked by + * shouldProcess or shouldLoad. + */ + +const POLICY = "default-src http://mochi.test:8888; object-src 'none'"; +const TESTFILE = "tests/dom/security/test/csp/file_shouldprocess.html"; + +SimpleTest.waitForExplicitFinish(); + +var tests = [ + // Note that the files listed below don't actually exist. + // Since loading of them should be blocked by shouldProcess, we don't + // really need these files. + + // blocked by shouldProcess + "http://mochi.test:8888/tests/dom/security/test/csp/test1", + "http://mochi.test:8888/tests/dom/security/test/csp/test2", + "http://mochi.test:8888/tests/dom/security/test/csp/test3", + "http://mochi.test:8888/tests/dom/security/test/csp/test4", + "http://mochi.test:8888/tests/dom/security/test/csp/test5", + "http://mochi.test:8888/tests/dom/security/test/csp/test6", + // blocked by shouldLoad + "http://mochi.test:8888/tests/dom/security/test/csp/test7.class", + "http://mochi.test:8888/tests/dom/security/test/csp/test8.class", +]; + +function checkResults(aURI) { + var index = tests.indexOf(aURI); + if (index > -1) { + tests.splice(index, 1); + ok(true, "ShouldLoad or ShouldProcess blocks TYPE_OBJECT with uri: " + aURI + "!"); + } + else { + ok(false, "ShouldLoad or ShouldProcess incorreclty blocks TYPE_OBJECT with uri: " + aURI + "!"); + } + if (tests.length == 0) { + window.examiner.remove(); + SimpleTest.finish(); + } +} + +// used to watch that shouldProcess blocks TYPE_OBJECT +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy", false); +} +examiner.prototype = { + observe: function(subject, topic, data) { + if (topic === "csp-on-violate-policy") { + var asciiSpec = + SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec"); + checkResults(asciiSpec); + } + }, + remove: function() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + } +} +window.examiner = new examiner(); + +function loadFrame() { + var src = "file_testserver.sjs"; + // append the file that should be served + src += "?file=" + escape(TESTFILE); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(POLICY); + + var iframe = document.createElement("iframe"); + iframe.src = src; + document.body.appendChild(iframe); +} + +SpecialPowers.pushPrefEnv( + { "set": [['plugin.java.mime', 'application/x-java-test']] }, + loadFrame); + +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/csp/test_strict_dynamic.html b/dom/security/test/csp/test_strict_dynamic.html new file mode 100644 index 000000000..00e75143f --- /dev/null +++ b/dom/security/test/csp/test_strict_dynamic.html @@ -0,0 +1,134 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.setBoolPref("security.csp.enableStrictDynamic", true); + +/* Description of the test: + * We load scripts with a CSP of 'strict-dynamic' with valid + * and invalid nonces and make sure scripts are allowed/blocked + * accordingly. Different tests load inline and external scripts + * also using a CSP including http: and https: making sure + * other srcs are invalided by 'strict-dynamic'. + */ + +var tests = [ + { + desc: "strict-dynamic with valid nonce should be allowed", + result: "allowed", + file: "file_strict_dynamic_script_extern.html", + policy: "script-src 'strict-dynamic' 'nonce-foo' https: 'none' 'self'" + }, + { + desc: "strict-dynamic with invalid nonce should be blocked", + result: "blocked", + file: "file_strict_dynamic_script_extern.html", + policy: "script-src 'strict-dynamic' 'nonce-bar' http: http://example.com" + }, + { + desc: "strict-dynamic, whitelist and invalid nonce should be blocked", + result: "blocked", + file: "file_strict_dynamic_script_extern.html", + policy: "script-src 'strict-dynamic' 'nonce-bar' 'unsafe-inline' http: http://example.com" + }, + { + desc: "strict-dynamic with no 'nonce-' should be blocked", + result: "blocked", + file: "file_strict_dynamic_script_extern.html", + policy: "script-src 'strict-dynamic'" + }, + // inline scripts + { + desc: "strict-dynamic with valid nonce should be allowed", + result: "allowed", + file: "file_strict_dynamic_script_inline.html", + policy: "script-src 'strict-dynamic' 'nonce-foo' https: 'none' 'self'" + }, + { + desc: "strict-dynamic with invalid nonce should be blocked", + result: "blocked", + file: "file_strict_dynamic_script_inline.html", + policy: "script-src 'strict-dynamic' 'nonce-bar' http: http://example.com" + }, + { + desc: "strict-dynamic, unsafe-inline and invalid nonce should be blocked", + result: "blocked", + file: "file_strict_dynamic_script_inline.html", + policy: "script-src 'strict-dynamic' 'nonce-bar' 'unsafe-inline' http: http://example.com" + }, + { + desc: "strict-dynamic with no 'nonce-' should be blocked", + result: "blocked", + file: "file_strict_dynamic_script_inline.html", + policy: "script-src 'strict-dynamic'" + }, + { + desc: "strict-dynamic with DOM events should be blocked", + result: "blocked", + file: "file_strict_dynamic_script_events.html", + policy: "script-src 'strict-dynamic' 'nonce-foo'" + }, + { + // marquee is a special snowflake. Extra test for xbl things. + desc: "strict-dynamic with DOM events should be blocked (XBL)", + result: "blocked", + file: "file_strict_dynamic_script_events_xbl.html", + policy: "script-src 'strict-dynamic' 'nonce-foo'" + }, + { + desc: "strict-dynamic with JS URLs should be blocked", + result: "blocked", + file: "file_strict_dynamic_js_url.html", + policy: "script-src 'strict-dynamic' 'nonce-foo'" + }, +]; + +var counter = 0; +var curTest; + +function loadNextTest() { + if (counter == tests.length) { + SimpleTest.finish(); + return; + } + + curTest = tests[counter++]; + var src = "file_testserver.sjs?file="; + // append the file that should be served + src += escape("tests/dom/security/test/csp/" + curTest.file) + // append the CSP that should be used to serve the file + src += "&csp=" + escape(curTest.policy); + + document.getElementById("testframe").addEventListener("load", test, false); + document.getElementById("testframe").src = src; +} + +function test() { + try { + document.getElementById("testframe").removeEventListener('load', test, false); + var testframe = document.getElementById("testframe"); + var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML; + is(divcontent, curTest.result, curTest.desc); + } + catch (e) { + ok(false, "ERROR: could not access content for test: '" + curTest.desc + "'"); + } + loadNextTest(); +} + +// start running the tests +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_strict_dynamic_default_src.html b/dom/security/test/csp/test_strict_dynamic_default_src.html new file mode 100644 index 000000000..17518444e --- /dev/null +++ b/dom/security/test/csp/test_strict_dynamic_default_src.html @@ -0,0 +1,136 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.setBoolPref("security.csp.enableStrictDynamic", true); + +/* Description of the test: + * We load scripts and images with a CSP of 'strict-dynamic' making sure + * whitelists get ignored for scripts but not for images when strict-dynamic + * appears in default-src. + * + * Please note that we do not support strict-dynamic within default-src yet, + * see Bug 1313937. When updating this test please do not change the + * csp policies, but only replace todo_is() with is(). + */ + +var tests = [ + { + script_desc: "(test1) script should be allowed because of valid nonce", + img_desc: "(test1) img should be allowed because of 'self'", + script_result: "allowed", + img_result: "allowed", + policy: "default-src 'strict-dynamic' 'self'; script-src 'nonce-foo'" + }, + { + script_desc: "(test 2) script should be blocked because of invalid nonce", + img_desc: "(test 2) img should be allowed because of valid scheme-src", + script_result: "blocked", + img_result: "allowed", + policy: "default-src 'strict-dynamic' http:; script-src 'nonce-bar' http:" + }, + { + script_desc: "(test 3) script should be blocked because of invalid nonce", + img_desc: "(test 3) img should be allowed because of valid host-src", + script_result: "blocked", + script_enforced: "", + img_result: "allowed", + policy: "default-src 'strict-dynamic' mochi.test; script-src 'nonce-bar' http:" + }, + { + script_desc: "(test 4) script should be allowed because of valid nonce", + img_desc: "(test 4) img should be blocked because of default-src 'strict-dynamic'", + script_result: "allowed", + img_result: "blocked", + policy: "default-src 'strict-dynamic'; script-src 'nonce-foo'" + }, + // some reverse order tests (have script-src appear before default-src) + { + script_desc: "(test 5) script should be allowed because of valid nonce", + img_desc: "(test 5) img should be blocked because of default-src 'strict-dynamic'", + script_result: "allowed", + img_result: "blocked", + policy: "script-src 'nonce-foo'; default-src 'strict-dynamic';" + }, + { + script_desc: "(test 6) script should be allowed because of valid nonce", + img_desc: "(test 6) img should be blocked because of default-src http:", + script_result: "blocked", + img_result: "allowed", + policy: "script-src 'nonce-bar' http:; default-src 'strict-dynamic' http:;" + }, + { + script_desc: "(test 7) script should be allowed because of invalid nonce", + img_desc: "(test 7) img should be blocked because of image-src http:", + script_result: "blocked", + img_result: "allowed", + policy: "script-src 'nonce-bar' http:; default-src 'strict-dynamic' http:; img-src http:" + }, +]; + +var counter = 0; +var curTest; + +function loadNextTest() { + if (counter == tests.length) { + SimpleTest.finish(); + return; + } + + curTest = tests[counter++]; + var src = "file_testserver.sjs?file="; + // append the file that should be served + src += escape("tests/dom/security/test/csp/file_strict_dynamic_default_src.html"); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(curTest.policy); + + document.getElementById("testframe").addEventListener("load", checkResults, false); + document.getElementById("testframe").src = src; +} + +function checkResults() { + try { + var testframe = document.getElementById("testframe"); + testframe.removeEventListener('load', checkResults, false); + + // check if script loaded + var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML; + if (curTest.script_result === "blocked") { + todo_is(divcontent, curTest.script_result, curTest.script_desc); + } + else { + is(divcontent, curTest.script_result, curTest.script_desc); + } + + // check if image loaded + var testimg = testframe.contentWindow.document.getElementById("testimage"); + if (curTest.img_result === "allowed") { + ok(testimg.complete, curTest.img_desc); + } + else { + ok((testimg.width == 0) && (testimg.height == 0), curTest.img_desc); + } + } + catch (e) { + ok(false, "ERROR: could not access content for test: '" + curTest.script_desc + "'"); + } + + loadNextTest(); +} + +// start running the tests +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_strict_dynamic_parser_inserted.html b/dom/security/test/csp/test_strict_dynamic_parser_inserted.html new file mode 100644 index 000000000..9e588660d --- /dev/null +++ b/dom/security/test/csp/test_strict_dynamic_parser_inserted.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.setBoolPref("security.csp.enableStrictDynamic", true); + +/* Description of the test: + * We loader parser and non parser inserted scripts making sure that + * parser inserted scripts are blocked if strict-dynamic is present + * and no valid nonce and also making sure that non-parser inserted + * scripts are allowed to execute. + */ + +var tests = [ + { + desc: "(parser inserted script) using doc.write(<script>) should be blocked", + result: "blocked", + file: "file_strict_dynamic_parser_inserted_doc_write.html", + policy: "script-src 'strict-dynamic' 'nonce-foo' http:" + }, + { + desc: "(parser inserted script with valid nonce) using doc.write(<script>) should be allowed", + result: "allowed", + file: "file_strict_dynamic_parser_inserted_doc_write_correct_nonce.html", + policy: "script-src 'strict-dynamic' 'nonce-foo' https:" + }, + { + desc: "(non parser inserted script) using appendChild() should allow external script", + result: "allowed", + file: "file_strict_dynamic_non_parser_inserted.html", + policy: "script-src 'strict-dynamic' 'nonce-foo' https:" + }, + { + desc: "(non parser inserted script) using appendChild() should allow inline script", + result: "allowed", + file: "file_strict_dynamic_non_parser_inserted_inline.html", + policy: "script-src 'strict-dynamic' 'nonce-foo' https:" + }, + { + desc: "strict-dynamic should not invalidate 'unsafe-eval'", + result: "allowed", + file: "file_strict_dynamic_unsafe_eval.html", + policy: "script-src 'strict-dynamic' 'nonce-foo' 'unsafe-eval'" + }, +]; + +var counter = 0; +var curTest; + +function loadNextTest() { + if (counter == tests.length) { + SimpleTest.finish(); + return; + } + + curTest = tests[counter++]; + var src = "file_testserver.sjs?file="; + // append the file that should be served + src += escape("tests/dom/security/test/csp/" + curTest.file) + // append the CSP that should be used to serve the file + src += "&csp=" + escape(curTest.policy); + + document.getElementById("testframe").addEventListener("load", test, false); + document.getElementById("testframe").src = src; +} + +function test() { + try { + document.getElementById("testframe").removeEventListener('load', test, false); + var testframe = document.getElementById("testframe"); + var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML; + is(divcontent, curTest.result, curTest.desc); + } + catch (e) { + ok(false, "ERROR: could not access content for test: '" + curTest.desc + "'"); + } + loadNextTest(); +} + +// start running the tests +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_subframe_run_js_if_allowed.html b/dom/security/test/csp/test_subframe_run_js_if_allowed.html new file mode 100644 index 000000000..ccc81a265 --- /dev/null +++ b/dom/security/test/csp/test_subframe_run_js_if_allowed.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=702439 + +This test verifies that child iframes of CSP documents are +permitted to execute javascript: URLs assuming the policy +allows this. +--> +<head> + <title>Test for Bug 702439</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe id="i"></iframe> +<script class="testbody" type="text/javascript"> +var javascript_link_ran = false; + +// check that the script in the child frame's javascript: URL ran +function checkResult() +{ + is(javascript_link_ran, true, + "javascript URL didn't execute"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +document.getElementById('i').src = 'file_subframe_run_js_if_allowed.html'; +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_upgrade_insecure.html b/dom/security/test/csp/test_upgrade_insecure.html new file mode 100644 index 000000000..a2b99b8db --- /dev/null +++ b/dom/security/test/csp/test_upgrade_insecure.html @@ -0,0 +1,181 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * We load resources (img, script, sytle, etc) over *http* and make sure + * that all the resources get upgraded to use >> https << when the + * csp-directive "upgrade-insecure-requests" is specified. We further + * test that subresources within nested contexts (iframes) get upgraded + * and also test the handling of server side redirects. + * + * In detail: + * We perform an XHR request to the *.sjs file which is processed async on + * the server and waits till all the requests were processed by the server. + * Once the server received all the different requests, the server responds + * to the initial XHR request with an array of results which must match + * the expected results from each test, making sure that all requests + * received by the server (*.sjs) were actually *https* requests. + */ + +const UPGRADE_POLICY = + "upgrade-insecure-requests;" + // upgrade all http requests to https + "block-all-mixed-content;" + // upgrade should be enforced before block-all. + "default-src https: wss: 'unsafe-inline';" + // only allow https: and wss: + "form-action https:;"; // explicit, no fallback to default-src + +const UPGRADE_POLICY_NO_DEFAULT_SRC = + "upgrade-insecure-requests;" + // upgrade all http requests to https + "script-src 'unsafe-inline' *"; // we have to whitelist the inline scripts + // in the test. +const NO_UPGRADE_POLICY = + "default-src http: ws: 'unsafe-inline';" + // allow http:// and ws:// + "form-action http:;"; // explicit, no fallback to default-src + +var tests = [ + { // (1) test that all requests within an >> https << page get updated + policy: UPGRADE_POLICY, + topLevelScheme: "https://", + description: "upgrade all requests on toplevel https", + deliveryMethod: "header", + results: [ + "iframe-ok", "script-ok", "img-ok", "img-redir-ok", "font-ok", "xhr-ok", "style-ok", + "media-ok", "object-ok", "form-ok", "websocket-ok", "nested-img-ok" + ] + }, + { // (2) test that all requests within an >> http << page get updated + policy: UPGRADE_POLICY, + topLevelScheme: "http://", + description: "upgrade all requests on toplevel http", + deliveryMethod: "header", + results: [ + "iframe-ok", "script-ok", "img-ok", "img-redir-ok", "font-ok", "xhr-ok", "style-ok", + "media-ok", "object-ok", "form-ok", "websocket-ok", "nested-img-ok" + ] + }, + { // (3) test that all requests within an >> http << page get updated, but do + // not specify a default-src directive. + policy: UPGRADE_POLICY_NO_DEFAULT_SRC, + topLevelScheme: "http://", + description: "upgrade all requests on toplevel http where default-src is not specified", + deliveryMethod: "header", + results: [ + "iframe-ok", "script-ok", "img-ok", "img-redir-ok", "font-ok", "xhr-ok", "style-ok", + "media-ok", "object-ok", "form-ok", "websocket-ok", "nested-img-ok" + ] + }, + { // (4) test that no requests get updated if >> upgrade-insecure-requests << is not used + policy: NO_UPGRADE_POLICY, + topLevelScheme: "http://", + description: "do not upgrade any requests on toplevel http", + deliveryMethod: "header", + results: [ + "iframe-error", "script-error", "img-error", "img-redir-error", "font-error", + "xhr-error", "style-error", "media-error", "object-error", "form-error", + "websocket-error", "nested-img-error" + ] + }, + { // (5) test that all requests within an >> https << page using meta CSP get updated + // policy: UPGRADE_POLICY, that test uses UPGRADE_POLICY within + // file_upgrade_insecure_meta.html + // no need to define it within that object. + topLevelScheme: "https://", + description: "upgrade all requests on toplevel https using meta csp", + deliveryMethod: "meta", + results: [ + "iframe-ok", "script-ok", "img-ok", "img-redir-ok", "font-ok", "xhr-ok", "style-ok", + "media-ok", "object-ok", "form-ok", "websocket-ok", "nested-img-ok" + ] + }, +]; + +var counter = 0; +var curTest; + +function loadTestPage() { + curTest = tests[counter++]; + var src = curTest.topLevelScheme + "example.com/tests/dom/security/test/csp/file_testserver.sjs?file="; + if (curTest.deliveryMethod === "header") { + // append the file that should be served + src += escape("tests/dom/security/test/csp/file_upgrade_insecure.html"); + // append the CSP that should be used to serve the file + src += "&csp=" + escape(curTest.policy); + } + else { + src += escape("tests/dom/security/test/csp/file_upgrade_insecure_meta.html"); + // no csp here, since it's in the meta element + } + document.getElementById("testframe").src = src; +} + +function finishTest() { + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); +} + +function checkResults(result) { + // try to find the expected result within the results array + var index = curTest.results.indexOf(result); + isnot(index, -1, curTest.description + " (result: " + result + ")"); + + // take the element out the array and continue till the results array is empty + if (index != -1) { + curTest.results.splice(index, 1); + } + // lets check if we are expecting more results to bubble up + if (curTest.results.length > 0) { + return; + } + // lets see if we ran all the tests + if (counter == tests.length) { + finishTest(); + return; + } + // otherwise it's time to run the next test + runNextTest(); +} + +// a postMessage handler that is used by sandboxed iframes without +// 'allow-same-origin' to bubble up results back to this main page. +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + checkResults(event.data.result); +} + +function runNextTest() { + // sends an xhr request to the server which is processed async, which only + // returns after the server has received all the expected requests. + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "file_upgrade_insecure_server.sjs?queryresult"); + myXHR.onload = function(e) { + var results = myXHR.responseText.split(","); + for (var index in results) { + checkResults(results[index]); + } + } + myXHR.onerror = function(e) { + ok(false, "could not query results from server (" + e.message + ")"); + finishTest(); + } + myXHR.send(); + + // give it some time and run the testpage + SimpleTest.executeSoon(loadTestPage); +} + +SimpleTest.waitForExplicitFinish(); +runNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_upgrade_insecure_cors.html b/dom/security/test/csp/test_upgrade_insecure_cors.html new file mode 100644 index 000000000..af296983c --- /dev/null +++ b/dom/security/test/csp/test_upgrade_insecure_cors.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * We load a page serving two XHR requests (including being redirected); + * one that should not require CORS and one that should require cors, in particular: + * + * Test 1: + * Main page: https://test1.example.com + * XHR request: http://test1.example.com + * Redirect to: http://test1.example.com + * Description: Upgrade insecure should upgrade from http to https and also + * surpress CORS for that case. + * + * Test 2: + * Main page: https://test1.example.com + * XHR request: http://test1.example.com + * Redirect to: http://test1.example.com:443 + * Description: Upgrade insecure should upgrade from http to https and also + * prevent CORS for that case. + * Note: If redirecting to a different port, then CORS *should* be enforced (unless + * it's port 443). Unfortunately we can't test that because of the setup of our + * *.sjs files; they only are able to listen to port 443, see: + * http://mxr.mozilla.org/mozilla-central/source/build/pgo/server-locations.txt#98 + * + * Test 3: + * Main page: https://test1.example.com + * XHR request: http://test2.example.com + * Redirect to: http://test1.example.com + * Description: Upgrade insecure should *not* prevent CORS since + * the page performs a cross origin xhr. + * + */ + +const CSP_POLICY = "upgrade-insecure-requests; script-src 'unsafe-inline'"; +var tests = 3; + +function loadTest() { + var src = "https://test1.example.com/tests/dom/security/test/csp/file_testserver.sjs?file="; + // append the file that should be served + src += escape("tests/dom/security/test/csp/file_upgrade_insecure_cors.html") + // append the CSP that should be used to serve the file + src += "&csp=" + escape(CSP_POLICY); + document.getElementById("testframe").src = src; +} + +function checkResult(result) { + if (result === "test1-no-cors-ok" || + result === "test2-no-cors-diffport-ok" || + result === "test3-cors-ok") { + ok(true, "'upgrade-insecure-requests' acknowledges CORS (" + result + ")"); + } + else { + ok(false, "'upgrade-insecure-requests' acknowledges CORS (" + result + ")"); + } + if (--tests > 0) { + return; + } + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); +} + +// a postMessage handler that is used to bubble up results from +// within the iframe. +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + checkResult(event.data); +} + +SimpleTest.waitForExplicitFinish(); +loadTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_upgrade_insecure_docwrite_iframe.html b/dom/security/test/csp/test_upgrade_insecure_docwrite_iframe.html new file mode 100644 index 000000000..822158bd7 --- /dev/null +++ b/dom/security/test/csp/test_upgrade_insecure_docwrite_iframe.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1273430 - Test CSP upgrade-insecure-requests for doc.write(iframe)</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * Load an iframe which ships with a CSP of upgrade-insecure-requests. + * Within that iframe a script performs doc.write(iframe) using an + * *http* URL. Make sure, the URL is upgraded to *https*. + * + * +-----------------------------------------+ + * | | + * | http(s); csp: upgrade-insecure-requests | | + * | +---------------------------------+ | + * | | | | + * | | doc.write(<iframe src='http'>); | <--------- upgrade to https + * | | | | + * | +---------------------------------+ | + * | | + * +-----------------------------------------+ + * + */ + +const TEST_FRAME_URL = + "https://example.com/tests/dom/security/test/csp/file_upgrade_insecure_docwrite_iframe.sjs?testframe"; + +// important: the RESULT should have a scheme of *https* +const RESULT = + "https://example.com/tests/dom/security/test/csp/file_upgrade_insecure_docwrite_iframe.sjs?docwriteframe"; + +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + is(event.data.result, RESULT, "doc.write(iframe) of http should be upgraded to https!"); + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); +} + +// start the test +SimpleTest.waitForExplicitFinish(); +var testframe = document.getElementById("testframe"); +testframe.src = TEST_FRAME_URL; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_upgrade_insecure_referrer.html b/dom/security/test/csp/test_upgrade_insecure_referrer.html new file mode 100644 index 000000000..890c57335 --- /dev/null +++ b/dom/security/test/csp/test_upgrade_insecure_referrer.html @@ -0,0 +1,85 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * We load a page that makes use of the CSP referrer directive as well + * as upgrade-insecure-requests. The page loads an image over http. + * The test makes sure the request gets upgraded to https and the + * correct referrer gets sent. + */ + +var tests = [ + { + query: "test1", + description: "upgrade insecure request with 'referrer = origin' (CSP in header)", + result: "http://example.com/" + }, + { + query: "test2", + description: "upgrade insecure request with 'referrer = no-referrer' (CSP in header)", + result: "" + }, + { + query: "test3", + description: "upgrade insecure request with 'referrer = origin' (Meta CSP)", + result: "http://example.com/" + }, + { + query: "test4", + description: "upgrade insecure request with 'referrer = no-referrer' (Meta CSP)", + result: "" + } +]; + +var counter = 0; +var curTest; + +function loadTestPage() { + curTest = tests[counter++]; + var src = "http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_referrer.sjs?"; + // append the query + src += curTest.query; + document.getElementById("testframe").src = src; +} + +function runNextTest() { + // sends a request to the server which is processed async and returns + // once the server received the expected image request + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "file_upgrade_insecure_referrer_server.sjs?queryresult"); + myXHR.onload = function(e) { + is(myXHR.responseText, curTest.result, curTest.description); + if (counter == tests.length) { + SimpleTest.finish(); + return; + } + // move on to the next test by setting off another query request. + runNextTest(); + } + myXHR.onerror = function(e) { + ok(false, "could not query results from server (" + e.message + ")"); + SimpleTest.finish(); + } + myXHR.send(); + + // give it some time and load the testpage + SimpleTest.executeSoon(loadTestPage); +} + +SimpleTest.waitForExplicitFinish(); +runNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_upgrade_insecure_reporting.html b/dom/security/test/csp/test_upgrade_insecure_reporting.html new file mode 100644 index 000000000..967654179 --- /dev/null +++ b/dom/security/test/csp/test_upgrade_insecure_reporting.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1139297 - Implement CSP upgrade-insecure-requests directive</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * We load an https page which includes an http image. We make sure that + * the image request gets upgraded to https but also make sure that a report + * is sent when a CSP report only is used which only allows https requests. + */ + +var expectedResults = 2; + +function finishTest() { + // let's wait till the image was loaded and the report was received + if (--expectedResults > 0) { + return; + } + window.removeEventListener("message", receiveMessage, false); + SimpleTest.finish(); +} + +function runTest() { + // (1) Lets send off an XHR request which will return once the server receives + // the violation report from the report only policy. + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "file_upgrade_insecure_reporting_server.sjs?queryresult"); + myXHR.onload = function(e) { + is(myXHR.responseText, "report-ok", "csp-report was sent correctly"); + finishTest(); + } + myXHR.onerror = function(e) { + ok(false, "could not query result for csp-report from server (" + e.message + ")"); + finishTest(); + } + myXHR.send(); + + // (2) We load a page that is served using a CSP and a CSP report only which loads + // an image over http. + SimpleTest.executeSoon(function() { + document.getElementById("testframe").src = + "https://example.com/tests/dom/security/test/csp/file_upgrade_insecure_reporting_server.sjs?toplevel"; + }); +} + +// a postMessage handler that is used by sandboxed iframes without +// 'allow-same-origin' to bubble up results back to this main page. +window.addEventListener("message", receiveMessage, false); +function receiveMessage(event) { + // (3) make sure the image was correctly loaded + is(event.data.result, "img-ok", "upgraded insecure image load from http -> https"); + finishTest(); +} + +SimpleTest.waitForExplicitFinish(); +runTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/general/bug1277803.html b/dom/security/test/general/bug1277803.html new file mode 100644 index 000000000..c8033551a --- /dev/null +++ b/dom/security/test/general/bug1277803.html @@ -0,0 +1,11 @@ +<html> + +<head> + <link rel='icon' href='favicon_bug1277803.ico'> +</head> + +<body> +Nothing to see here... +</body> + +</html> diff --git a/dom/security/test/general/chrome.ini b/dom/security/test/general/chrome.ini new file mode 100644 index 000000000..94bf1ef05 --- /dev/null +++ b/dom/security/test/general/chrome.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + favicon_bug1277803.ico + bug1277803.html + +[test_bug1277803.xul] +skip-if = os == 'android' diff --git a/dom/security/test/general/favicon_bug1277803.ico b/dom/security/test/general/favicon_bug1277803.ico Binary files differnew file mode 100644 index 000000000..d44438903 --- /dev/null +++ b/dom/security/test/general/favicon_bug1277803.ico diff --git a/dom/security/test/general/file_block_script_wrong_mime_server.sjs b/dom/security/test/general/file_block_script_wrong_mime_server.sjs new file mode 100644 index 000000000..d6d27796c --- /dev/null +++ b/dom/security/test/general/file_block_script_wrong_mime_server.sjs @@ -0,0 +1,34 @@ +// Custom *.sjs specifically for the needs of: +// Bug 1288361 - Block scripts with wrong MIME type + +"use strict"; +Components.utils.importGlobalProperties(["URLSearchParams"]); + +const WORKER = ` + onmessage = function(event) { + postMessage("worker-loaded"); + };`; + +function handleRequest(request, response) { + const query = new URLSearchParams(request.queryString); + + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // Set MIME type + response.setHeader("Content-Type", query.get("mime"), false); + + // Deliver response + switch (query.get("type")) { + case "script": + response.write(""); + break; + case "worker": + response.write(WORKER); + break; + case "worker-import": + response.write(`importScripts("file_block_script_wrong_mime_server.sjs?type=script&mime=${query.get("mime")}");`); + response.write(WORKER); + break; + } +} diff --git a/dom/security/test/general/file_contentpolicytype_targeted_link_iframe.sjs b/dom/security/test/general/file_contentpolicytype_targeted_link_iframe.sjs new file mode 100644 index 000000000..f0084410a --- /dev/null +++ b/dom/security/test/general/file_contentpolicytype_targeted_link_iframe.sjs @@ -0,0 +1,46 @@ +// custom *.sjs for Bug 1255240 + +const TEST_FRAME = ` + <!DOCTYPE HTML> + <html> + <head><meta charset='utf-8'></head> + <body> + <a id='testlink' target='innerframe' href='file_contentpolicytype_targeted_link_iframe.sjs?innerframe'>click me</a> + <iframe name='innerframe'></iframe> + <script type='text/javascript'> + var link = document.getElementById('testlink'); + testlink.click(); + </script> + </body> + </html> `; + +const INNER_FRAME = ` + <!DOCTYPE HTML> + <html> + <head><meta charset='utf-8'></head> + hello world! + </body> + </html>`; + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + var queryString = request.queryString; + + if (queryString === "testframe") { + response.write(TEST_FRAME); + return; + } + + if (queryString === "innerframe") { + response.write(INNER_FRAME); + return; + } + + // we should never get here, but just in case + // return something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/general/file_nosniff_testserver.sjs b/dom/security/test/general/file_nosniff_testserver.sjs new file mode 100644 index 000000000..0cf168a3c --- /dev/null +++ b/dom/security/test/general/file_nosniff_testserver.sjs @@ -0,0 +1,60 @@ +"use strict"; +Components.utils.importGlobalProperties(["URLSearchParams"]); + +const SCRIPT = "var foo = 24;"; +const CSS = "body { background-color: green; }"; + +// small red image +const IMG = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="); + +function handleRequest(request, response) { + const query = new URLSearchParams(request.queryString); + + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // set the nosniff header + response.setHeader("X-Content-Type-Options", " NoSniFF , foo ", false); + + if (query.has("cssCorrectType")) { + response.setHeader("Content-Type", "teXt/cSs", false); + response.write(CSS); + return; + } + + if (query.has("cssWrongType")) { + response.setHeader("Content-Type", "text/html", false); + response.write(CSS); + return; + } + + if (query.has("scriptCorrectType")) { + response.setHeader("Content-Type", "appLIcation/jAvaScriPt;blah", false); + response.write(SCRIPT); + return; + } + + if (query.has("scriptWrongType")) { + response.setHeader("Content-Type", "text/html", false); + response.write(SCRIPT); + return; + } + + if (query.has("imgCorrectType")) { + response.setHeader("Content-Type", "iMaGe/pnG;blah", false); + response.write(IMG); + return; + } + + if (query.has("imgWrongType")) { + response.setHeader("Content-Type", "text/html", false); + response.write(IMG); + return; + } + + // we should never get here, but just in case + response.setHeader("Content-Type", "text/html", false); + response.write("do'h"); +} diff --git a/dom/security/test/general/mochitest.ini b/dom/security/test/general/mochitest.ini new file mode 100644 index 000000000..70c0c9fb6 --- /dev/null +++ b/dom/security/test/general/mochitest.ini @@ -0,0 +1,9 @@ +[DEFAULT] +support-files = + file_contentpolicytype_targeted_link_iframe.sjs + file_nosniff_testserver.sjs + file_block_script_wrong_mime_server.sjs + +[test_contentpolicytype_targeted_link_iframe.html] +[test_nosniff.html] +[test_block_script_wrong_mime.html] diff --git a/dom/security/test/general/test_block_script_wrong_mime.html b/dom/security/test/general/test_block_script_wrong_mime.html new file mode 100644 index 000000000..f4da9c577 --- /dev/null +++ b/dom/security/test/general/test_block_script_wrong_mime.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1288361 - Block scripts with incorrect MIME type</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script class="testbody" type="text/javascript"> + +const MIMETypes = [ + ["application/javascript", true], + ["text/javascript", true], + + ["audio/mpeg", false], + ["audio/", false], + ["image/jpeg", false], + ["image/", false], + ["video/mpeg", false], + ["video/", false], + ["text/csv", false], +]; + +// <script src=""> +function testScript([mime, shouldLoad]) { + return new Promise((resolve, reject) => { + let script = document.createElement("script"); + script.onload = () => { + document.body.removeChild(script); + ok(shouldLoad, `script with mime '${mime}' should load`); + resolve(); + }; + script.onerror = () => { + document.body.removeChild(script); + ok(!shouldLoad, `script with wrong mime '${mime}' should be blocked`); + resolve(); + }; + script.src = "file_block_script_wrong_mime_server.sjs?type=script&mime="+mime; + document.body.appendChild(script); + }); +} + +// new Worker() +function testWorker([mime, shouldLoad]) { + return new Promise((resolve, reject) => { + let worker = new Worker("file_block_script_wrong_mime_server.sjs?type=worker&mime="+mime); + worker.onmessage = (event) => { + ok(shouldLoad, `worker with mime '${mime}' should load`) + is(event.data, "worker-loaded", "worker should send correct message"); + resolve(); + }; + worker.onerror = (error) => { + ok(!shouldLoad, `worker with wrong mime '${mime}' should be blocked`); + let msg = error.message; + ok(msg.match(/^NetworkError/) || msg.match(/Failed to load worker script/), + "should gets correct error message"); + error.preventDefault(); + resolve(); + } + worker.postMessage("dummy"); + }); +} + +// new Worker() with importScripts() +function testWorkerImportScripts([mime, shouldLoad]) { + return new Promise((resolve, reject) => { + let worker = new Worker("file_block_script_wrong_mime_server.sjs?type=worker-import&mime="+mime); + worker.onmessage = (event) => { + ok(shouldLoad, `worker/importScripts with mime '${mime}' should load`) + is(event.data, "worker-loaded", "worker should send correct message"); + resolve(); + }; + worker.onerror = (error) => { + ok(!shouldLoad, `worker/importScripts with wrong mime '${mime}' should be blocked`); + let msg = error.message; + ok(msg.match(/^NetworkError/) || msg.match(/Failed to load worker script/), + "should gets correct error message"); + error.preventDefault(); + resolve(); + } + worker.postMessage("dummy"); + }); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({set: [["security.block_script_with_wrong_mime", true]]}, function() { + Promise.all(MIMETypes.map(testScript)).then(() => { + return Promise.all(MIMETypes.map(testWorker)); + }).then(() => { + return Promise.all(MIMETypes.map(testWorkerImportScripts)); + }).then(() => { + SpecialPowers.popPrefEnv(SimpleTest.finish); + }); +}); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_bug1277803.xul b/dom/security/test/general/test_bug1277803.xul new file mode 100644 index 000000000..a62285f8a --- /dev/null +++ b/dom/security/test/general/test_bug1277803.xul @@ -0,0 +1,99 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Bug 1277803 test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="runTest();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + </body> + + <script type="application/javascript"><![CDATA[ + SimpleTest.requestCompleteLog(); + let Ci = Components.interfaces; + let Cc = Components.classes; + let Cu = Components.utils; + let makeURI = Cu.import("resource://gre/modules/BrowserUtils.jsm", {}).BrowserUtils.makeURI; + + const BASE_URI = "http://mochi.test:8888/chrome/dom/security/test/general/"; + const FAVICON_URI = BASE_URI + "favicon_bug1277803.ico"; + const LOADING_URI = BASE_URI + "bug1277803.html"; + let testWindow; //will be used to trigger favicon load + + let securityManager = Cc["@mozilla.org/scriptsecuritymanager;1"]. + getService(Ci.nsIScriptSecurityManager); + let expectedPrincipal = securityManager.createCodebasePrincipal(makeURI(LOADING_URI), {}); + let systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance(); + + // We expect 2 favicon loads, one from PlacesUIUtils.loadFavicon and one + // from XUL:image loads. + let requestXUL = false; + let requestPlaces = false; + + function runTest() { + // Register our observer to intercept favicon requests. + let os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + let observer = { + observe: function(aSubject, aTopic, aData) + { + // Make sure this is a favicon request. + let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel); + if (FAVICON_URI != httpChannel.URI.spec) { + return; + } + + // Ensure the topic is the one we set an observer for. + is(aTopic, "http-on-modify-request", "Expected observer topic"); + + // Check for the correct loadingPrincipal, triggeringPrincipal. + let triggeringPrincipal = httpChannel.loadInfo.triggeringPrincipal; + let loadingPrincipal = httpChannel.loadInfo.loadingPrincipal; + + if (loadingPrincipal.equals(systemPrincipal)) { + // This is the favicon loading from XUL, which will have the system + // principal as its loading principal and have a content principal + // as its triggering principal. + ok(triggeringPrincipal.equals(expectedPrincipal), + "Correct triggeringPrincipal for favicon from XUL."); + requestXUL = true; + } else if (loadingPrincipal.equals(expectedPrincipal)) { + // This is the favicon loading from Places, which will have a + // content principal as its loading principal and triggering + // principal. + ok(triggeringPrincipal.equals(expectedPrincipal), + "Correct triggeringPrincipal for favicon from Places."); + requestPlaces = true; + } else { + ok(false, "An unexpected favicon request.") + } + + // Cleanup after ourselves... + if (requestXUL && requestPlaces) { + os.removeObserver(this, "http-on-modify-request"); + SimpleTest.finish(); + } + } + } + os.addObserver(observer, "http-on-modify-request", false); + + // Now that the observer is set up, trigger a favicon load with navigation + testWindow = window.open(LOADING_URI); + } + + SimpleTest.waitForExplicitFinish(); + SimpleTest.registerCleanupFunction(function() { + if (testWindow) { + testWindow.close(); + } + }); + ]]></script> + + <browser type="content-primary" flex="1" id="content" src="about:blank"/> +</window> diff --git a/dom/security/test/general/test_contentpolicytype_targeted_link_iframe.html b/dom/security/test/general/test_contentpolicytype_targeted_link_iframe.html new file mode 100644 index 000000000..7b1ab72dc --- /dev/null +++ b/dom/security/test/general/test_contentpolicytype_targeted_link_iframe.html @@ -0,0 +1,92 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1255240 - Test content policy types within content policies for targeted links in iframes</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* Description of the test: + * Let's load a link into a targeted iframe and make sure the content policy + * type used for content policy checks is of TYPE_SUBDOCUMENT. + */ + +const Cc = SpecialPowers.Cc; +const Ci = SpecialPowers.Ci; + +const EXPECTED_CONTENT_TYPE = Ci.nsIContentPolicy.TYPE_SUBDOCUMENT; +const EXPECTED_URL = + "http://mochi.test:8888/tests/dom/security/test/general/file_contentpolicytype_targeted_link_iframe.sjs?innerframe"; +const TEST_FRAME_URL = + "file_contentpolicytype_targeted_link_iframe.sjs?testframe"; + +// ----- START Content Policy implementation for the test +var categoryManager = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager); + +const POLICYNAME = "@mozilla.org/iframetestpolicy;1"; +const POLICYID = SpecialPowers.wrap(SpecialPowers.Components) + .ID("{6cc95ef3-40e1-4d59-87f0-86f100373227}"); + +var policy = { + // nsISupports implementation + QueryInterface: function(iid) { + iid = SpecialPowers.wrap(iid); + if (iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIFactory) || + iid.equals(Ci.nsIContentPolicy)) + return this; + + throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE; + }, + + // nsIFactory implementation + createInstance: function(outer, iid) { + return this.QueryInterface(iid); + }, + + // nsIContentPolicy implementation + shouldLoad: function(contentType, contentLocation, requestOrigin, + context, mimeTypeGuess, extra) { + + // make sure we get the right amount of content policy calls + // e.g. about:blank also gets chrcked by content policies + if (contentLocation.asciiSpec === EXPECTED_URL) { + is(contentType, EXPECTED_CONTENT_TYPE, + "content policy type should TYPESUBDOCUMENT"); + categoryManager.deleteCategoryEntry("content-policy", POLICYNAME, false); + SimpleTest.finish(); + } + return Ci.nsIContentPolicy.ACCEPT; + }, + + shouldProcess: function(contentType, contentLocation, requestOrigin, + context, mimeTypeGuess, extra) { + return Ci.nsIContentPolicy.ACCEPT; + } +} +policy = SpecialPowers.wrapCallbackObject(policy); + +// Register content policy +var componentManager = SpecialPowers.wrap(SpecialPowers.Components).manager + .QueryInterface(Ci.nsIComponentRegistrar); + +componentManager.registerFactory(POLICYID, "Test content policy", POLICYNAME, policy); +categoryManager.addCategoryEntry("content-policy", POLICYNAME, POLICYNAME, false, true); + +// ----- END Content Policy implementation for the test + + +// start the test +SimpleTest.waitForExplicitFinish(); +var testframe = document.getElementById("testframe"); +testframe.src = TEST_FRAME_URL; + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_nosniff.html b/dom/security/test/general/test_nosniff.html new file mode 100644 index 000000000..197251e68 --- /dev/null +++ b/dom/security/test/general/test_nosniff.html @@ -0,0 +1,118 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 471020 - Add X-Content-Type-Options: nosniff support to Firefox</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + + <!-- add the two css tests --> + <link rel="stylesheet" id="cssCorrectType"> + <link rel="stylesheet" id="cssWrongType"> +</head> +<body> + +<!-- add the two script tests --> +<script id="scriptCorrectType"></script> +<script id="scriptWrongType"></script> + +<!-- add the two img tests --> +<img id="imgCorrectType" /> +<img id="imgWrongType" /> + +<script class="testbody" type="text/javascript"> +/* Description of the test: + * We load 2 css files, 2 script files and 2 image files, where + * the sever either responds with the right mime type or + * the wrong mime type for each test. + */ + +SimpleTest.waitForExplicitFinish(); +const NUM_TESTS = 6; + +var testCounter = 0; +function checkFinish() { + testCounter++; + if (testCounter === NUM_TESTS) { + SimpleTest.finish(); + } +} + +SpecialPowers.pushPrefEnv({set: [["security.xcto_nosniff_block_images", true]]}, function() { + + // 1) Test CSS with correct mime type + var cssCorrectType = document.getElementById("cssCorrectType"); + cssCorrectType.onload = function() { + ok(true, "style nosniff correct type should load"); + checkFinish(); + } + cssCorrectType.onerror = function() { + ok(false, "style nosniff correct type should load"); + checkFinish(); + } + cssCorrectType.href = "file_nosniff_testserver.sjs?cssCorrectType"; + + // 2) Test CSS with wrong mime type + var cssWrongType = document.getElementById("cssWrongType"); + cssWrongType.onload = function() { + ok(false, "style nosniff wrong type should not load"); + checkFinish(); + } + cssWrongType.onerror = function() { + ok(true, "style nosniff wrong type should not load"); + checkFinish(); + } + cssWrongType.href = "file_nosniff_testserver.sjs?cssWrongType"; + + // 3) Test SCRIPT with correct mime type + var scriptCorrectType = document.getElementById("scriptCorrectType"); + scriptCorrectType.onload = function() { + ok(true, "script nosniff correct type should load"); + checkFinish(); + } + scriptCorrectType.onerror = function() { + ok(false, "script nosniff correct type should load"); + checkFinish(); + } + scriptCorrectType.src = "file_nosniff_testserver.sjs?scriptCorrectType"; + + // 4) Test SCRIPT with wrong mime type + var scriptWrongType = document.getElementById("scriptWrongType"); + scriptWrongType.onload = function() { + ok(false, "script nosniff wrong type should not load"); + checkFinish(); + } + scriptWrongType.onerror = function() { + ok(true, "script nosniff wrong type should not load"); + checkFinish(); + } + scriptWrongType.src = "file_nosniff_testserver.sjs?scriptWrongType"; + + // 5) Test IMG with correct mime type + var imgCorrectType = document.getElementById("imgCorrectType"); + imgCorrectType.onload = function() { + ok(true, "img nosniff correct type should load"); + checkFinish(); + } + imgCorrectType.onerror = function() { + ok(false, "img nosniff correct type should load"); + checkFinish(); + } + imgCorrectType.src = "file_nosniff_testserver.sjs?imgCorrectType"; + + // 6) Test IMG with wrong mime type + var imgWrongType = document.getElementById("imgWrongType"); + imgWrongType.onload = function() { + ok(false, "img nosniff wrong type should not load"); + checkFinish(); + } + imgWrongType.onerror = function() { + ok(true, "img nosniff wrong type should not load"); + checkFinish(); + } + imgWrongType.src = "file_nosniff_testserver.sjs?imgWrongType"; +}); + +</script> +</body> +</html> diff --git a/dom/security/test/gtest/TestCSPParser.cpp b/dom/security/test/gtest/TestCSPParser.cpp new file mode 100644 index 000000000..fafa7b5d9 --- /dev/null +++ b/dom/security/test/gtest/TestCSPParser.cpp @@ -0,0 +1,1132 @@ +/* -*- 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 "gtest/gtest.h" + +#include <string.h> +#include <stdlib.h> + +#ifndef MOZILLA_INTERNAL_API +// some of the includes make use of internal string types +#define nsAString_h___ +#define nsString_h___ +#define nsStringFwd_h___ +#define nsReadableUtils_h___ +class nsACString; +class nsAString; +class nsAFlatString; +class nsAFlatCString; +class nsAdoptingString; +class nsAdoptingCString; +class nsXPIDLString; +template<class T> class nsReadingIterator; +#endif + +#include "nsIContentSecurityPolicy.h" +#include "nsNetUtil.h" +#include "nsIScriptSecurityManager.h" +#include "mozilla/dom/nsCSPContext.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" + +#ifndef MOZILLA_INTERNAL_API +#undef nsString_h___ +#undef nsAString_h___ +#undef nsReadableUtils_h___ +#endif + +/* + * Testing the parser is non trivial, especially since we can not call + * parser functionality directly in compiled code tests. + * All the tests (except the fuzzy tests at the end) follow the same schemata: + * a) create an nsIContentSecurityPolicy object + * b) set the selfURI in SetRequestContext + * c) append one or more policies by calling AppendPolicy + * d) check if the policy count is correct by calling GetPolicyCount + * e) compare the result of the policy with the expected output + * using the struct PolicyTest; + * + * In general we test: + * a) policies that the parser should accept + * b) policies that the parser should reject + * c) policies that are randomly generated (fuzzy tests) + * + * Please note that fuzzy tests are *DISABLED* by default and shold only + * be run *OFFLINE* whenever code in nsCSPParser changes. + * To run fuzzy tests, flip RUN_OFFLINE_TESTS to 1. + * + */ + +#define RUN_OFFLINE_TESTS 0 + +/* + * Offline tests are separated in three different groups: + * * TestFuzzyPolicies - complete random ASCII input + * * TestFuzzyPoliciesIncDir - a directory name followed by random ASCII + * * TestFuzzyPoliciesIncDirLimASCII - a directory name followed by limited ASCII + * which represents more likely user input. + * + * We run each of this categories |kFuzzyRuns| times. + */ + +#if RUN_OFFLINE_TESTS +static const uint32_t kFuzzyRuns = 10000; +#endif + +// For fuzzy testing we actually do not care about the output, +// we just want to make sure that the parser can handle random +// input, therefore we use kFuzzyExpectedPolicyCount to return early. +static const uint32_t kFuzzyExpectedPolicyCount = 111; + +static const uint32_t kMaxPolicyLength = 96; + +struct PolicyTest +{ + char policy[kMaxPolicyLength]; + char expectedResult[kMaxPolicyLength]; +}; + +nsresult runTest(uint32_t aExpectedPolicyCount, // this should be 0 for policies which should fail to parse + const char* aPolicy, + const char* aExpectedResult) { + + nsresult rv; + nsCOMPtr<nsIScriptSecurityManager> secman = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // we init the csp with http://www.selfuri.com + nsCOMPtr<nsIURI> selfURI; + rv = NS_NewURI(getter_AddRefs(selfURI), "http://www.selfuri.com"); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrincipal> selfURIPrincipal; + // Can't use BasePrincipal::CreateCodebasePrincipal here + // because the symbol is not visible here + rv = secman->GetCodebasePrincipal(selfURI, getter_AddRefs(selfURIPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + + // create a CSP object + nsCOMPtr<nsIContentSecurityPolicy> csp = + do_CreateInstance(NS_CSPCONTEXT_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // for testing the parser we only need to set a principal which is needed + // to translate the keyword 'self' into an actual URI. + rv = csp->SetRequestContext(nullptr, selfURIPrincipal); + NS_ENSURE_SUCCESS(rv, rv); + + // append a policy + nsString policyStr; + policyStr.AssignASCII(aPolicy); + rv = csp->AppendPolicy(policyStr, false, false); + NS_ENSURE_SUCCESS(rv, rv); + + // when executing fuzzy tests we do not care about the actual output + // of the parser, we just want to make sure that the parser is not crashing. + if (aExpectedPolicyCount == kFuzzyExpectedPolicyCount) { + return NS_OK; + } + + // verify that the expected number of policies exists + uint32_t actualPolicyCount; + rv = csp->GetPolicyCount(&actualPolicyCount); + NS_ENSURE_SUCCESS(rv, rv); + if (actualPolicyCount != aExpectedPolicyCount) { + EXPECT_TRUE(false) << + "Actual policy count not equal to expected policy count (" << + actualPolicyCount << " != " << aExpectedPolicyCount << + ") for policy: " << aPolicy; + return NS_ERROR_UNEXPECTED; + } + + // if the expected policy count is 0, we can return, because + // we can not compare any output anyway. Used when parsing + // errornous policies. + if (aExpectedPolicyCount == 0) { + return NS_OK; + } + + // compare the parsed policy against the expected result + nsString parsedPolicyStr; + // checking policy at index 0, which is the one what we appended. + rv = csp->GetPolicyString(0, parsedPolicyStr); + NS_ENSURE_SUCCESS(rv, rv); + + if (!NS_ConvertUTF16toUTF8(parsedPolicyStr).EqualsASCII(aExpectedResult)) { + EXPECT_TRUE(false) << + "Actual policy does not match expected policy (" << + NS_ConvertUTF16toUTF8(parsedPolicyStr).get() << " != " << + aExpectedResult << ")"; + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +// ============================= run Tests ======================== + +nsresult runTestSuite(const PolicyTest* aPolicies, + uint32_t aPolicyCount, + uint32_t aExpectedPolicyCount) { + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + bool experimentalEnabledCache = false; + bool strictDynamicEnabledCache = false; + if (prefs) + { + prefs->GetBoolPref("security.csp.experimentalEnabled", &experimentalEnabledCache); + prefs->SetBoolPref("security.csp.experimentalEnabled", true); + + prefs->GetBoolPref("security.csp.enableStrictDynamic", &strictDynamicEnabledCache); + prefs->SetBoolPref("security.csp.enableStrictDynamic", true); + } + + for (uint32_t i = 0; i < aPolicyCount; i++) { + rv = runTest(aExpectedPolicyCount, aPolicies[i].policy, aPolicies[i].expectedResult); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (prefs) { + prefs->SetBoolPref("security.csp.experimentalEnabled", experimentalEnabledCache); + prefs->SetBoolPref("security.csp.enableStrictDynamic", strictDynamicEnabledCache); + } + + return NS_OK; +} + +// ============================= TestDirectives ======================== + +TEST(CSPParser, Directives) +{ + static const PolicyTest policies[] = + { + { "default-src http://www.example.com", + "default-src http://www.example.com" }, + { "script-src http://www.example.com", + "script-src http://www.example.com" }, + { "object-src http://www.example.com", + "object-src http://www.example.com" }, + { "style-src http://www.example.com", + "style-src http://www.example.com" }, + { "img-src http://www.example.com", + "img-src http://www.example.com" }, + { "media-src http://www.example.com", + "media-src http://www.example.com" }, + { "frame-src http://www.example.com", + "frame-src http://www.example.com" }, + { "font-src http://www.example.com", + "font-src http://www.example.com" }, + { "connect-src http://www.example.com", + "connect-src http://www.example.com" }, + { "report-uri http://www.example.com", + "report-uri http://www.example.com/" }, + { "script-src 'nonce-correctscriptnonce'", + "script-src 'nonce-correctscriptnonce'" }, + { "script-src 'sha256-siVR8vAcqP06h2ppeNwqgjr0yZ6yned4X2VF84j4GmI='", + "script-src 'sha256-siVR8vAcqP06h2ppeNwqgjr0yZ6yned4X2VF84j4GmI='" }, + { "referrer no-referrer", + "referrer no-referrer" }, + { "require-sri-for script style", + "require-sri-for script style"}, + { "script-src 'nonce-foo' 'unsafe-inline' ", + "script-src 'nonce-foo' 'unsafe-inline'" }, + { "script-src 'nonce-foo' 'strict-dynamic' 'unsafe-inline' https: ", + "script-src 'nonce-foo' 'strict-dynamic' 'unsafe-inline' https:" }, + { "default-src 'sha256-siVR8' 'strict-dynamic' 'unsafe-inline' https: ", + "default-src 'sha256-siVR8' 'unsafe-inline' https:" }, + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_TRUE(NS_SUCCEEDED(runTestSuite(policies, policyCount, 1))); +} + +// ============================= TestKeywords ======================== + +TEST(CSPParser, Keywords) +{ + static const PolicyTest policies[] = + { + { "script-src 'self'", + "script-src http://www.selfuri.com" }, + { "script-src 'unsafe-inline'", + "script-src 'unsafe-inline'" }, + { "script-src 'unsafe-eval'", + "script-src 'unsafe-eval'" }, + { "script-src 'unsafe-inline' 'unsafe-eval'", + "script-src 'unsafe-inline' 'unsafe-eval'" }, + { "script-src 'none'", + "script-src 'none'" }, + { "img-src 'none'; script-src 'unsafe-eval' 'unsafe-inline'; default-src 'self'", + "img-src 'none'; script-src 'unsafe-eval' 'unsafe-inline'; default-src http://www.selfuri.com" }, + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_TRUE(NS_SUCCEEDED(runTestSuite(policies, policyCount, 1))); +} + +// ============================= TestIgnoreUpperLowerCasePolicies ======================== + +TEST(CSPParser, IgnoreUpperLowerCasePolicies) +{ + static const PolicyTest policies[] = + { + { "script-src 'SELF'", + "script-src http://www.selfuri.com" }, + { "sCriPt-src 'Unsafe-Inline'", + "script-src 'unsafe-inline'" }, + { "SCRIPT-src 'unsafe-eval'", + "script-src 'unsafe-eval'" }, + { "default-SRC 'unsafe-inline' 'unsafe-eval'", + "default-src 'unsafe-inline' 'unsafe-eval'" }, + { "script-src 'NoNe'", + "script-src 'none'" }, + { "img-sRc 'noNe'; scrIpt-src 'unsafe-EVAL' 'UNSAFE-inline'; deFAULT-src 'Self'", + "img-src 'none'; script-src 'unsafe-eval' 'unsafe-inline'; default-src http://www.selfuri.com" }, + { "default-src HTTP://www.example.com", + "default-src http://www.example.com" }, + { "default-src HTTP://WWW.EXAMPLE.COM", + "default-src http://www.example.com" }, + { "default-src HTTPS://*.example.COM", + "default-src https://*.example.com" }, + { "script-src 'none' test.com;", + "script-src http://test.com" }, + { "script-src 'NoNCE-correctscriptnonce'", + "script-src 'nonce-correctscriptnonce'" }, + { "script-src 'NoncE-NONCENEEDSTOBEUPPERCASE'", + "script-src 'nonce-NONCENEEDSTOBEUPPERCASE'" }, + { "script-src 'SHA256-siVR8vAcqP06h2ppeNwqgjr0yZ6yned4X2VF84j4GmI='", + "script-src 'sha256-siVR8vAcqP06h2ppeNwqgjr0yZ6yned4X2VF84j4GmI='" }, + { "refERRer No-refeRRer", + "referrer no-referrer" }, + { "upgrade-INSECURE-requests", + "upgrade-insecure-requests" }, + { "sanDBox alloW-foRMs", + "sandbox allow-forms"}, + { "require-SRI-for sCript stYle", + "require-sri-for script style"}, + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_TRUE(NS_SUCCEEDED(runTestSuite(policies, policyCount, 1))); +} + +// ============================= TestPaths ======================== + +TEST(CSPParser, Paths) +{ + static const PolicyTest policies[] = + { + { "script-src http://www.example.com", + "script-src http://www.example.com" }, + { "script-src http://www.example.com/", + "script-src http://www.example.com/" }, + { "script-src http://www.example.com/path-1", + "script-src http://www.example.com/path-1" }, + { "script-src http://www.example.com/path-1/", + "script-src http://www.example.com/path-1/" }, + { "script-src http://www.example.com/path-1/path_2", + "script-src http://www.example.com/path-1/path_2" }, + { "script-src http://www.example.com/path-1/path_2/", + "script-src http://www.example.com/path-1/path_2/" }, + { "script-src http://www.example.com/path-1/path_2/file.js", + "script-src http://www.example.com/path-1/path_2/file.js" }, + { "script-src http://www.example.com/path-1/path_2/file_1.js", + "script-src http://www.example.com/path-1/path_2/file_1.js" }, + { "script-src http://www.example.com/path-1/path_2/file-2.js", + "script-src http://www.example.com/path-1/path_2/file-2.js" }, + { "script-src http://www.example.com/path-1/path_2/f.js", + "script-src http://www.example.com/path-1/path_2/f.js" }, + { "script-src http://www.example.com:88", + "script-src http://www.example.com:88" }, + { "script-src http://www.example.com:88/", + "script-src http://www.example.com:88/" }, + { "script-src http://www.example.com:88/path-1", + "script-src http://www.example.com:88/path-1" }, + { "script-src http://www.example.com:88/path-1/", + "script-src http://www.example.com:88/path-1/" }, + { "script-src http://www.example.com:88/path-1/path_2", + "script-src http://www.example.com:88/path-1/path_2" }, + { "script-src http://www.example.com:88/path-1/path_2/", + "script-src http://www.example.com:88/path-1/path_2/" }, + { "script-src http://www.example.com:88/path-1/path_2/file.js", + "script-src http://www.example.com:88/path-1/path_2/file.js" }, + { "script-src http://www.example.com:*", + "script-src http://www.example.com:*" }, + { "script-src http://www.example.com:*/", + "script-src http://www.example.com:*/" }, + { "script-src http://www.example.com:*/path-1", + "script-src http://www.example.com:*/path-1" }, + { "script-src http://www.example.com:*/path-1/", + "script-src http://www.example.com:*/path-1/" }, + { "script-src http://www.example.com:*/path-1/path_2", + "script-src http://www.example.com:*/path-1/path_2" }, + { "script-src http://www.example.com:*/path-1/path_2/", + "script-src http://www.example.com:*/path-1/path_2/" }, + { "script-src http://www.example.com:*/path-1/path_2/file.js", + "script-src http://www.example.com:*/path-1/path_2/file.js" }, + { "script-src http://www.example.com#foo", + "script-src http://www.example.com" }, + { "script-src http://www.example.com?foo=bar", + "script-src http://www.example.com" }, + { "script-src http://www.example.com:8888#foo", + "script-src http://www.example.com:8888" }, + { "script-src http://www.example.com:8888?foo", + "script-src http://www.example.com:8888" }, + { "script-src http://www.example.com/#foo", + "script-src http://www.example.com/" }, + { "script-src http://www.example.com/?foo", + "script-src http://www.example.com/" }, + { "script-src http://www.example.com/path-1/file.js#foo", + "script-src http://www.example.com/path-1/file.js" }, + { "script-src http://www.example.com/path-1/file.js?foo", + "script-src http://www.example.com/path-1/file.js" }, + { "script-src http://www.example.com/path-1/file.js?foo#bar", + "script-src http://www.example.com/path-1/file.js" }, + { "report-uri http://www.example.com/", + "report-uri http://www.example.com/" }, + { "report-uri http://www.example.com:8888/asdf", + "report-uri http://www.example.com:8888/asdf" }, + { "report-uri http://www.example.com:8888/path_1/path_2", + "report-uri http://www.example.com:8888/path_1/path_2" }, + { "report-uri http://www.example.com:8888/path_1/path_2/report.sjs&301", + "report-uri http://www.example.com:8888/path_1/path_2/report.sjs&301" }, + { "report-uri /examplepath", + "report-uri http://www.selfuri.com/examplepath" }, + { "connect-src http://www.example.com/foo%3Bsessionid=12%2C34", + "connect-src http://www.example.com/foo;sessionid=12,34" }, + { "connect-src http://www.example.com/foo%3bsessionid=12%2c34", + "connect-src http://www.example.com/foo;sessionid=12,34" }, + { "connect-src http://test.com/pathIncludingAz19-._~!$&'()*+=:@", + "connect-src http://test.com/pathIncludingAz19-._~!$&'()*+=:@" }, + { "script-src http://www.example.com:88/.js", + "script-src http://www.example.com:88/.js" }, + { "script-src https://foo.com/_abc/abc_/_/_a_b_c_", + "script-src https://foo.com/_abc/abc_/_/_a_b_c_" } + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_TRUE(NS_SUCCEEDED(runTestSuite(policies, policyCount, 1))); +} + +// ============================= TestSimplePolicies ======================== + +TEST(CSPParser, SimplePolicies) +{ + static const PolicyTest policies[] = + { + { "default-src *", + "default-src *" }, + { "default-src https:", + "default-src https:" }, + { "default-src https://*", + "default-src https://*" }, + { "default-src *:*", + "default-src http://*:*" }, + { "default-src *:80", + "default-src http://*:80" }, + { "default-src http://*:80", + "default-src http://*:80" }, + { "default-src javascript:", + "default-src javascript:" }, + { "default-src data:", + "default-src data:" }, + { "script-src 'unsafe-eval' 'unsafe-inline' http://www.example.com", + "script-src 'unsafe-eval' 'unsafe-inline' http://www.example.com" }, + { "object-src 'self'", + "object-src http://www.selfuri.com" }, + { "style-src http://www.example.com 'self'", + "style-src http://www.example.com http://www.selfuri.com" }, + { "media-src http://www.example.com http://www.test.com", + "media-src http://www.example.com http://www.test.com" }, + { "connect-src http://www.test.com example.com *.other.com;", + "connect-src http://www.test.com http://example.com http://*.other.com"}, + { "connect-src example.com *.other.com", + "connect-src http://example.com http://*.other.com"}, + { "style-src *.other.com example.com", + "style-src http://*.other.com http://example.com"}, + { "default-src 'self'; img-src *;", + "default-src http://www.selfuri.com; img-src *" }, + { "object-src media1.example.com media2.example.com *.cdn.example.com;", + "object-src http://media1.example.com http://media2.example.com http://*.cdn.example.com" }, + { "script-src trustedscripts.example.com", + "script-src http://trustedscripts.example.com" }, + { "script-src 'self' ; default-src trustedscripts.example.com", + "script-src http://www.selfuri.com; default-src http://trustedscripts.example.com" }, + { "default-src 'none'; report-uri http://localhost:49938/test", + "default-src 'none'; report-uri http://localhost:49938/test" }, + { "default-src app://{app-host-is-uid}", + "default-src app://{app-host-is-uid}" }, + { " ; default-src abc", + "default-src http://abc" }, + { " ; ; ; ; default-src abc ; ; ; ;", + "default-src http://abc" }, + { "script-src 'none' 'none' 'none';", + "script-src 'none'" }, + { "script-src http://www.example.com/path-1//", + "script-src http://www.example.com/path-1//" }, + { "script-src http://www.example.com/path-1//path_2", + "script-src http://www.example.com/path-1//path_2" }, + { "default-src 127.0.0.1", + "default-src http://127.0.0.1" }, + { "default-src 127.0.0.1:*", + "default-src http://127.0.0.1:*" }, + { "default-src -; ", + "default-src http://-" }, + { "script-src 1", + "script-src http://1" }, + { "upgrade-insecure-requests", + "upgrade-insecure-requests" }, + { "upgrade-insecure-requests https:", + "upgrade-insecure-requests" }, + { "sandbox allow-scripts allow-forms ", + "sandbox allow-scripts allow-forms" }, + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_TRUE(NS_SUCCEEDED(runTestSuite(policies, policyCount, 1))); +} + +// ============================= TestPoliciesWithInvalidSrc ======================== + +TEST(CSPParser, PoliciesWithInvalidSrc) +{ + static const PolicyTest policies[] = + { + { "script-src 'self'; SCRIPT-SRC http://www.example.com", + "script-src http://www.selfuri.com" }, + { "script-src 'none' test.com; script-src example.com", + "script-src http://test.com" }, + { "default-src **", + "default-src 'none'" }, + { "default-src 'self", + "default-src 'none'" }, + { "default-src 'unsafe-inlin' ", + "default-src 'none'" }, + { "default-src */", + "default-src 'none'" }, + { "default-src", + "default-src 'none'" }, + { "default-src 'unsafe-inlin' ", + "default-src 'none'" }, + { "default-src :88", + "default-src 'none'" }, + { "script-src abc::::::88", + "script-src 'none'" }, + { "script-src *.*:*", + "script-src 'none'" }, + { "img-src *::88", + "img-src 'none'" }, + { "object-src http://localhost:", + "object-src 'none'" }, + { "script-src test..com", + "script-src 'none'" }, + { "script-src sub1.sub2.example+", + "script-src 'none'" }, + { "script-src http://www.example.com//", + "script-src 'none'" }, + { "script-src http://www.example.com:88path-1/", + "script-src 'none'" }, + { "script-src http://www.example.com:88//", + "script-src 'none'" }, + { "script-src http://www.example.com:88//path-1", + "script-src 'none'" }, + { "script-src http://www.example.com:88//path-1", + "script-src 'none'" }, + { "script-src http://www.example.com:88.js", + "script-src 'none'" }, + { "script-src http://www.example.com:*.js", + "script-src 'none'" }, + { "script-src http://www.example.com:*.", + "script-src 'none'" }, + { "connect-src http://www.example.com/foo%zz;", + "connect-src 'none'" }, + { "script-src https://foo.com/%$", + "script-src 'none'" }, + { "require-SRI-for script elephants", + "require-sri-for script"}, + { "sandbox foo", + "sandbox"}, + }; + + // amount of tests - 1, because the latest should be ignored. + uint32_t policyCount = (sizeof(policies) / sizeof(PolicyTest)) -1; + ASSERT_TRUE(NS_SUCCEEDED(runTestSuite(policies, policyCount, 1))); +} + +// ============================= TestBadPolicies ======================== + +TEST(CSPParser, BadPolicies) +{ + static const PolicyTest policies[] = + { + { "script-sr 'self", "" }, + { "", "" }, + { "; ; ; ; ; ; ;", "" }, + { "defaut-src asdf", "" }, + { "default-src: aaa", "" }, + { "asdf http://test.com", ""}, + { "referrer", ""}, + { "referrer foo", ""}, + { "require-sri-for", ""}, + { "require-sri-for foo", ""}, + { "report-uri", ""}, + { "report-uri http://:foo", ""}, + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_TRUE(NS_SUCCEEDED(runTestSuite(policies, policyCount, 0))); +} + +// ============================= TestGoodGeneratedPolicies ======================== + +TEST(CSPParser, GoodGeneratedPolicies) +{ + static const PolicyTest policies[] = + { + { "default-src 'self'; img-src *", + "default-src http://www.selfuri.com; img-src *" }, + { "report-uri /policy", + "report-uri http://www.selfuri.com/policy"}, + { "img-src *", + "img-src *" }, + { "media-src foo.bar", + "media-src http://foo.bar" }, + { "frame-src *.bar", + "frame-src http://*.bar" }, + { "font-src com", + "font-src http://com" }, + { "connect-src f00b4r.com", + "connect-src http://f00b4r.com" }, + { "default-src {app-url-is-uid}", + "default-src http://{app-url-is-uid}" }, + { "script-src *.a.b.c", + "script-src http://*.a.b.c" }, + { "object-src *.b.c", + "object-src http://*.b.c" }, + { "style-src a.b.c", + "style-src http://a.b.c" }, + { "img-src a.com", + "img-src http://a.com" }, + { "media-src http://abc.com", + "media-src http://abc.com" }, + { "frame-src a2-c.com", + "frame-src http://a2-c.com" }, + { "font-src https://a.com", + "font-src https://a.com" }, + { "connect-src *.a.com", + "connect-src http://*.a.com" }, + { "default-src a.com:23", + "default-src http://a.com:23" }, + { "script-src https://a.com:200", + "script-src https://a.com:200" }, + { "object-src data:", + "object-src data:" }, + { "style-src javascript:", + "style-src javascript:" }, + { "img-src {app-host-is-uid}", + "img-src http://{app-host-is-uid}" }, + { "media-src app://{app-host-is-uid}", + "media-src app://{app-host-is-uid}" }, + { "frame-src https://foobar.com:443", + "frame-src https://foobar.com:443" }, + { "font-src https://a.com:443", + "font-src https://a.com:443" }, + { "connect-src http://a.com:80", + "connect-src http://a.com:80" }, + { "default-src http://foobar.com", + "default-src http://foobar.com" }, + { "script-src https://foobar.com", + "script-src https://foobar.com" }, + { "object-src https://{app-host-is-uid}", + "object-src https://{app-host-is-uid}" }, + { "style-src 'none'", + "style-src 'none'" }, + { "img-src foo.bar:21 https://ras.bar", + "img-src http://foo.bar:21 https://ras.bar" }, + { "media-src http://foo.bar:21 https://ras.bar:443", + "media-src http://foo.bar:21 https://ras.bar:443" }, + { "frame-src http://self.com:80", + "frame-src http://self.com:80" }, + { "font-src http://self.com", + "font-src http://self.com" }, + { "connect-src https://foo.com http://bar.com:88", + "connect-src https://foo.com http://bar.com:88" }, + { "default-src * https://bar.com 'none'", + "default-src * https://bar.com" }, + { "script-src *.foo.com", + "script-src http://*.foo.com" }, + { "object-src http://b.com", + "object-src http://b.com" }, + { "style-src http://bar.com:88", + "style-src http://bar.com:88" }, + { "img-src https://bar.com:88", + "img-src https://bar.com:88" }, + { "media-src http://bar.com:443", + "media-src http://bar.com:443" }, + { "frame-src https://foo.com:88", + "frame-src https://foo.com:88" }, + { "font-src http://foo.com", + "font-src http://foo.com" }, + { "connect-src http://x.com:23", + "connect-src http://x.com:23" }, + { "default-src http://barbaz.com", + "default-src http://barbaz.com" }, + { "script-src http://somerandom.foo.com", + "script-src http://somerandom.foo.com" }, + { "default-src *", + "default-src *" }, + { "style-src http://bar.com:22", + "style-src http://bar.com:22" }, + { "img-src https://foo.com:443", + "img-src https://foo.com:443" }, + { "script-src https://foo.com; ", + "script-src https://foo.com" }, + { "img-src bar.com:*", + "img-src http://bar.com:*" }, + { "font-src https://foo.com:400", + "font-src https://foo.com:400" }, + { "connect-src http://bar.com:400", + "connect-src http://bar.com:400" }, + { "default-src http://evil.com", + "default-src http://evil.com" }, + { "script-src https://evil.com:100", + "script-src https://evil.com:100" }, + { "default-src bar.com; script-src https://foo.com", + "default-src http://bar.com; script-src https://foo.com" }, + { "default-src 'self'; script-src 'self' https://*:*", + "default-src http://www.selfuri.com; script-src http://www.selfuri.com https://*:*" }, + { "img-src http://self.com:34", + "img-src http://self.com:34" }, + { "media-src http://subd.self.com:34", + "media-src http://subd.self.com:34" }, + { "default-src 'none'", + "default-src 'none'" }, + { "connect-src http://self", + "connect-src http://self" }, + { "default-src http://foo", + "default-src http://foo" }, + { "script-src http://foo:80", + "script-src http://foo:80" }, + { "object-src http://bar", + "object-src http://bar" }, + { "style-src http://three:80", + "style-src http://three:80" }, + { "img-src https://foo:400", + "img-src https://foo:400" }, + { "media-src https://self:34", + "media-src https://self:34" }, + { "frame-src https://bar", + "frame-src https://bar" }, + { "font-src http://three:81", + "font-src http://three:81" }, + { "connect-src https://three:81", + "connect-src https://three:81" }, + { "script-src http://self.com:80/foo", + "script-src http://self.com:80/foo" }, + { "object-src http://self.com/foo", + "object-src http://self.com/foo" }, + { "report-uri /report.py", + "report-uri http://www.selfuri.com/report.py"}, + { "img-src http://foo.org:34/report.py", + "img-src http://foo.org:34/report.py" }, + { "media-src foo/bar/report.py", + "media-src http://foo/bar/report.py" }, + { "report-uri /", + "report-uri http://www.selfuri.com/"}, + { "font-src https://self.com/report.py", + "font-src https://self.com/report.py" }, + { "connect-src https://foo.com/report.py", + "connect-src https://foo.com/report.py" }, + { "default-src *; report-uri http://www.reporturi.com/", + "default-src *; report-uri http://www.reporturi.com/" }, + { "default-src http://first.com", + "default-src http://first.com" }, + { "script-src http://second.com", + "script-src http://second.com" }, + { "object-src http://third.com", + "object-src http://third.com" }, + { "style-src https://foobar.com:4443", + "style-src https://foobar.com:4443" }, + { "img-src http://foobar.com:4443", + "img-src http://foobar.com:4443" }, + { "media-src bar.com", + "media-src http://bar.com" }, + { "frame-src http://bar.com", + "frame-src http://bar.com" }, + { "font-src http://self.com/", + "font-src http://self.com/" }, + { "script-src 'self'", + "script-src http://www.selfuri.com" }, + { "default-src http://self.com/foo.png", + "default-src http://self.com/foo.png" }, + { "script-src http://self.com/foo.js", + "script-src http://self.com/foo.js" }, + { "object-src http://bar.com/foo.js", + "object-src http://bar.com/foo.js" }, + { "style-src http://FOO.COM", + "style-src http://foo.com" }, + { "img-src HTTP", + "img-src http://http" }, + { "media-src http", + "media-src http://http" }, + { "frame-src 'SELF'", + "frame-src http://www.selfuri.com" }, + { "DEFAULT-src 'self';", + "default-src http://www.selfuri.com" }, + { "default-src 'self' http://FOO.COM", + "default-src http://www.selfuri.com http://foo.com" }, + { "default-src 'self' HTTP://foo.com", + "default-src http://www.selfuri.com http://foo.com" }, + { "default-src 'NONE'", + "default-src 'none'" }, + { "script-src policy-uri ", + "script-src http://policy-uri" }, + { "img-src 'self'; ", + "img-src http://www.selfuri.com" }, + { "frame-ancestors foo-bar.com", + "frame-ancestors http://foo-bar.com" }, + { "frame-ancestors http://a.com", + "frame-ancestors http://a.com" }, + { "frame-ancestors 'self'", + "frame-ancestors http://www.selfuri.com" }, + { "frame-ancestors http://self.com:88", + "frame-ancestors http://self.com:88" }, + { "frame-ancestors http://a.b.c.d.e.f.g.h.i.j.k.l.x.com", + "frame-ancestors http://a.b.c.d.e.f.g.h.i.j.k.l.x.com" }, + { "frame-ancestors https://self.com:34", + "frame-ancestors https://self.com:34" }, + { "default-src 'none'; frame-ancestors 'self'", + "default-src 'none'; frame-ancestors http://www.selfuri.com" }, + { "frame-ancestors http://self:80", + "frame-ancestors http://self:80" }, + { "frame-ancestors http://self.com/bar", + "frame-ancestors http://self.com/bar" }, + { "default-src 'self'; frame-ancestors 'self'", + "default-src http://www.selfuri.com; frame-ancestors http://www.selfuri.com" }, + { "frame-ancestors http://bar.com/foo.png", + "frame-ancestors http://bar.com/foo.png" }, + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_TRUE(NS_SUCCEEDED(runTestSuite(policies, policyCount, 1))); +} + +// ============================= TestBadGeneratedPolicies ======================== + +TEST(CSPParser, BadGeneratedPolicies) +{ + static const PolicyTest policies[] = + { + { "foo.*.bar", ""}, + { "foo!bar.com", ""}, + { "x.*.a.com", ""}, + { "a#2-c.com", ""}, + { "http://foo.com:bar.com:23", ""}, + { "f!oo.bar", ""}, + { "ht!ps://f-oo.bar", ""}, + { "https://f-oo.bar:3f", ""}, + { "**", ""}, + { "*a", ""}, + { "http://username:password@self.com/foo", ""}, + { "http://other:pass1@self.com/foo", ""}, + { "http://user1:pass1@self.com/foo", ""}, + { "http://username:password@self.com/bar", ""}, + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_TRUE(NS_SUCCEEDED(runTestSuite(policies, policyCount, 0))); +} + +// ============ TestGoodGeneratedPoliciesForPathHandling ============ + +TEST(CSPParser, GoodGeneratedPoliciesForPathHandling) +{ + // Once bug 808292 (Implement path-level host-source matching to CSP) + // lands we have to update the expected output to include the parsed path + + static const PolicyTest policies[] = + { + { "img-src http://test1.example.com", + "img-src http://test1.example.com" }, + { "img-src http://test1.example.com/", + "img-src http://test1.example.com/" }, + { "img-src http://test1.example.com/path-1", + "img-src http://test1.example.com/path-1" }, + { "img-src http://test1.example.com/path-1/", + "img-src http://test1.example.com/path-1/" }, + { "img-src http://test1.example.com/path-1/path_2/", + "img-src http://test1.example.com/path-1/path_2/" }, + { "img-src http://test1.example.com/path-1/path_2/file.js", + "img-src http://test1.example.com/path-1/path_2/file.js" }, + { "img-src http://test1.example.com/path-1/path_2/file_1.js", + "img-src http://test1.example.com/path-1/path_2/file_1.js" }, + { "img-src http://test1.example.com/path-1/path_2/file-2.js", + "img-src http://test1.example.com/path-1/path_2/file-2.js" }, + { "img-src http://test1.example.com/path-1/path_2/f.js", + "img-src http://test1.example.com/path-1/path_2/f.js" }, + { "img-src http://test1.example.com/path-1/path_2/f.oo.js", + "img-src http://test1.example.com/path-1/path_2/f.oo.js" }, + { "img-src test1.example.com", + "img-src http://test1.example.com" }, + { "img-src test1.example.com/", + "img-src http://test1.example.com/" }, + { "img-src test1.example.com/path-1", + "img-src http://test1.example.com/path-1" }, + { "img-src test1.example.com/path-1/", + "img-src http://test1.example.com/path-1/" }, + { "img-src test1.example.com/path-1/path_2/", + "img-src http://test1.example.com/path-1/path_2/" }, + { "img-src test1.example.com/path-1/path_2/file.js", + "img-src http://test1.example.com/path-1/path_2/file.js" }, + { "img-src test1.example.com/path-1/path_2/file_1.js", + "img-src http://test1.example.com/path-1/path_2/file_1.js" }, + { "img-src test1.example.com/path-1/path_2/file-2.js", + "img-src http://test1.example.com/path-1/path_2/file-2.js" }, + { "img-src test1.example.com/path-1/path_2/f.js", + "img-src http://test1.example.com/path-1/path_2/f.js" }, + { "img-src test1.example.com/path-1/path_2/f.oo.js", + "img-src http://test1.example.com/path-1/path_2/f.oo.js" }, + { "img-src *.example.com", + "img-src http://*.example.com" }, + { "img-src *.example.com/", + "img-src http://*.example.com/" }, + { "img-src *.example.com/path-1", + "img-src http://*.example.com/path-1" }, + { "img-src *.example.com/path-1/", + "img-src http://*.example.com/path-1/" }, + { "img-src *.example.com/path-1/path_2/", + "img-src http://*.example.com/path-1/path_2/" }, + { "img-src *.example.com/path-1/path_2/file.js", + "img-src http://*.example.com/path-1/path_2/file.js" }, + { "img-src *.example.com/path-1/path_2/file_1.js", + "img-src http://*.example.com/path-1/path_2/file_1.js" }, + { "img-src *.example.com/path-1/path_2/file-2.js", + "img-src http://*.example.com/path-1/path_2/file-2.js" }, + { "img-src *.example.com/path-1/path_2/f.js", + "img-src http://*.example.com/path-1/path_2/f.js" }, + { "img-src *.example.com/path-1/path_2/f.oo.js", + "img-src http://*.example.com/path-1/path_2/f.oo.js" }, + { "img-src test1.example.com:80", + "img-src http://test1.example.com:80" }, + { "img-src test1.example.com:80/", + "img-src http://test1.example.com:80/" }, + { "img-src test1.example.com:80/path-1", + "img-src http://test1.example.com:80/path-1" }, + { "img-src test1.example.com:80/path-1/", + "img-src http://test1.example.com:80/path-1/" }, + { "img-src test1.example.com:80/path-1/path_2", + "img-src http://test1.example.com:80/path-1/path_2" }, + { "img-src test1.example.com:80/path-1/path_2/", + "img-src http://test1.example.com:80/path-1/path_2/" }, + { "img-src test1.example.com:80/path-1/path_2/file.js", + "img-src http://test1.example.com:80/path-1/path_2/file.js" }, + { "img-src test1.example.com:80/path-1/path_2/f.ile.js", + "img-src http://test1.example.com:80/path-1/path_2/f.ile.js" }, + { "img-src test1.example.com:*", + "img-src http://test1.example.com:*" }, + { "img-src test1.example.com:*/", + "img-src http://test1.example.com:*/" }, + { "img-src test1.example.com:*/path-1", + "img-src http://test1.example.com:*/path-1" }, + { "img-src test1.example.com:*/path-1/", + "img-src http://test1.example.com:*/path-1/" }, + { "img-src test1.example.com:*/path-1/path_2", + "img-src http://test1.example.com:*/path-1/path_2" }, + { "img-src test1.example.com:*/path-1/path_2/", + "img-src http://test1.example.com:*/path-1/path_2/" }, + { "img-src test1.example.com:*/path-1/path_2/file.js", + "img-src http://test1.example.com:*/path-1/path_2/file.js" }, + { "img-src test1.example.com:*/path-1/path_2/f.ile.js", + "img-src http://test1.example.com:*/path-1/path_2/f.ile.js" }, + { "img-src http://test1.example.com/abc//", + "img-src http://test1.example.com/abc//" }, + { "img-src https://test1.example.com/abc/def//", + "img-src https://test1.example.com/abc/def//" }, + { "img-src https://test1.example.com/abc/def/ghi//", + "img-src https://test1.example.com/abc/def/ghi//" }, + { "img-src http://test1.example.com:80/abc//", + "img-src http://test1.example.com:80/abc//" }, + { "img-src https://test1.example.com:80/abc/def//", + "img-src https://test1.example.com:80/abc/def//" }, + { "img-src https://test1.example.com:80/abc/def/ghi//", + "img-src https://test1.example.com:80/abc/def/ghi//" }, + { "img-src https://test1.example.com/abc////////////def/", + "img-src https://test1.example.com/abc////////////def/" }, + { "img-src https://test1.example.com/abc////////////", + "img-src https://test1.example.com/abc////////////" }, + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_TRUE(NS_SUCCEEDED(runTestSuite(policies, policyCount, 1))); +} + +// ============ TestBadGeneratedPoliciesForPathHandling ============ + +TEST(CSPParser, BadGeneratedPoliciesForPathHandling) +{ + static const PolicyTest policies[] = + { + { "img-src test1.example.com:88path-1/", + "img-src 'none'" }, + { "img-src test1.example.com:80.js", + "img-src 'none'" }, + { "img-src test1.example.com:*.js", + "img-src 'none'" }, + { "img-src test1.example.com:*.", + "img-src 'none'" }, + { "img-src http://test1.example.com//", + "img-src 'none'" }, + { "img-src http://test1.example.com:80//", + "img-src 'none'" }, + { "img-src http://test1.example.com:80abc", + "img-src 'none'" }, + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_TRUE(NS_SUCCEEDED(runTestSuite(policies, policyCount, 1))); +} + +// ============================= TestFuzzyPolicies ======================== + +// Use a policy, eliminate one character at a time, +// and feed it as input to the parser. + +TEST(CSPParser, ShorteningPolicies) +{ + char pol[] = "default-src http://www.sub1.sub2.example.com:88/path1/path2/ 'unsafe-inline' 'none'"; + uint32_t len = static_cast<uint32_t>(sizeof(pol)); + + PolicyTest testPol[1]; + memset(&testPol[0].policy, '\0', kMaxPolicyLength * sizeof(char)); + + while (--len) { + memset(&testPol[0].policy, '\0', kMaxPolicyLength * sizeof(char)); + memcpy(&testPol[0].policy, &pol, len * sizeof(char)); + ASSERT_TRUE(NS_SUCCEEDED(runTestSuite(testPol, 1, + kFuzzyExpectedPolicyCount))); + } +} + +// ============================= TestFuzzyPolicies ======================== + +// We generate kFuzzyRuns inputs by (pseudo) randomly picking from the 128 +// ASCII characters; feed them to the parser and verfy that the parser +// handles the input gracefully. +// +// Please note, that by using srand(0) we get deterministic results! + +#if RUN_OFFLINE_TESTS + +TEST(CSPParser, FuzzyPolicies) +{ + // init srand with 0 so we get same results + srand(0); + + PolicyTest testPol[1]; + memset(&testPol[0].policy, '\0', kMaxPolicyLength); + + for (uint32_t index = 0; index < kFuzzyRuns; index++) { + // randomly select the length of the next policy + uint32_t polLength = rand() % kMaxPolicyLength; + // reset memory of the policy string + memset(&testPol[0].policy, '\0', kMaxPolicyLength * sizeof(char)); + + for (uint32_t i = 0; i < polLength; i++) { + // fill the policy array with random ASCII chars + testPol[0].policy[i] = static_cast<char>(rand() % 128); + } + ASSERT_TRUE(NS_SUCCEEDED(runTestSuite(testPol, 1, + kFuzzyExpectedPolicyCount))); + } +} + +#endif + +// ============================= TestFuzzyPoliciesIncDir ======================== + +// In a similar fashion as in TestFuzzyPolicies, we again (pseudo) randomly +// generate input for the parser, but this time also include a valid directive +// followed by the random input. + +#if RUN_OFFLINE_TESTS + +TEST(CSPParser, FuzzyPoliciesIncDir) +{ + // init srand with 0 so we get same results + srand(0); + + PolicyTest testPol[1]; + memset(&testPol[0].policy, '\0', kMaxPolicyLength); + + char defaultSrc[] = "default-src "; + int defaultSrcLen = sizeof(defaultSrc) - 1; + // copy default-src into the policy array + memcpy(&testPol[0].policy, &defaultSrc, (defaultSrcLen * sizeof(char))); + + for (uint32_t index = 0; index < kFuzzyRuns; index++) { + // randomly select the length of the next policy + uint32_t polLength = rand() % (kMaxPolicyLength - defaultSrcLen); + // reset memory of the policy string, but leave default-src. + memset((&(testPol[0].policy) + (defaultSrcLen * sizeof(char))), + '\0', (kMaxPolicyLength - defaultSrcLen) * sizeof(char)); + + // do not start at index 0 so we do not overwrite 'default-src' + for (uint32_t i = defaultSrcLen; i < polLength; i++) { + // fill the policy array with random ASCII chars + testPol[0].policy[i] = static_cast<char>(rand() % 128); + } + ASSERT_TRUE(NS_SUCCEEDED(runTestSuite(testPol, 1, + kFuzzyExpectedPolicyCount))); + } +} + +#endif + +// ============================= TestFuzzyPoliciesIncDirLimASCII ============== + +// Same as TestFuzzyPoliciesIncDir() but using limited ASCII, +// which represents more likely input. + +#if RUN_OFFLINE_TESTS + +TEST(CSPParser, FuzzyPoliciesIncDirLimASCII) +{ + char input[] = "1234567890" \ + "abcdefghijklmnopqrstuvwxyz" \ + "ABCDEFGHIJKLMNOPQRSTUVWZYZ" \ + "!@#^&*()-+_="; + + // init srand with 0 so we get same results + srand(0); + + PolicyTest testPol[1]; + memset(&testPol[0].policy, '\0', kMaxPolicyLength); + + char defaultSrc[] = "default-src "; + int defaultSrcLen = sizeof(defaultSrc) - 1; + // copy default-src into the policy array + memcpy(&testPol[0].policy, &defaultSrc, (defaultSrcLen * sizeof(char))); + + for (uint32_t index = 0; index < kFuzzyRuns; index++) { + // randomly select the length of the next policy + uint32_t polLength = rand() % (kMaxPolicyLength - defaultSrcLen); + // reset memory of the policy string, but leave default-src. + memset((&(testPol[0].policy) + (defaultSrcLen * sizeof(char))), + '\0', (kMaxPolicyLength - defaultSrcLen) * sizeof(char)); + + // do not start at index 0 so we do not overwrite 'default-src' + for (uint32_t i = defaultSrcLen; i < polLength; i++) { + // fill the policy array with chars from the pre-defined input + uint32_t inputIndex = rand() % sizeof(input); + testPol[0].policy[i] = input[inputIndex]; + } + ASSERT_TRUE(NS_SUCCEEDED(runTestSuite(testPol, 1, + kFuzzyExpectedPolicyCount))); + } +} +#endif + diff --git a/dom/security/test/gtest/moz.build b/dom/security/test/gtest/moz.build new file mode 100644 index 000000000..e927e7bfa --- /dev/null +++ b/dom/security/test/gtest/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES += [ + 'TestCSPParser.cpp', +] + +FINAL_LIBRARY = 'xul-gtest' diff --git a/dom/security/test/hsts/browser.ini b/dom/security/test/hsts/browser.ini new file mode 100644 index 000000000..ae75031df --- /dev/null +++ b/dom/security/test/hsts/browser.ini @@ -0,0 +1,19 @@ +[DEFAULT] +skip-if = debug # bug 1311599, bug 1311239, etc +support-files = + head.js + file_priming-top.html + file_testserver.sjs + file_1x1.png + file_priming.js + file_stylesheet.css + +[browser_hsts-priming_allow_active.js] +[browser_hsts-priming_block_active.js] +[browser_hsts-priming_hsts_after_mixed.js] +[browser_hsts-priming_allow_display.js] +[browser_hsts-priming_block_display.js] +[browser_hsts-priming_block_active_css.js] +[browser_hsts-priming_block_active_with_redir_same.js] +[browser_hsts-priming_no-duplicates.js] +[browser_hsts-priming_cache-timeout.js] diff --git a/dom/security/test/hsts/browser_hsts-priming_allow_active.js b/dom/security/test/hsts/browser_hsts-priming_allow_active.js new file mode 100644 index 000000000..a932b31b3 --- /dev/null +++ b/dom/security/test/hsts/browser_hsts-priming_allow_active.js @@ -0,0 +1,24 @@ +/* + * Description of the test: + * Check that HSTS priming occurs correctly with mixed content when active + * content is allowed. + */ +'use strict'; + +//jscs:disable +add_task(function*() { + //jscs:enable + Services.obs.addObserver(Observer, "console-api-log-event", false); + Services.obs.addObserver(Observer, "http-on-examine-response", false); + registerCleanupFunction(do_cleanup); + + let which = "allow_active"; + + SetupPrefTestEnvironment(which); + + for (let server of Object.keys(test_servers)) { + yield execute_test(server, test_settings[which].mimetype); + } + + SpecialPowers.popPrefEnv(); +}); diff --git a/dom/security/test/hsts/browser_hsts-priming_allow_display.js b/dom/security/test/hsts/browser_hsts-priming_allow_display.js new file mode 100644 index 000000000..06546ca65 --- /dev/null +++ b/dom/security/test/hsts/browser_hsts-priming_allow_display.js @@ -0,0 +1,24 @@ +/* + * Description of the test: + * Check that HSTS priming occurs correctly with mixed content when display + * content is allowed. + */ +'use strict'; + +//jscs:disable +add_task(function*() { + //jscs:enable + Services.obs.addObserver(Observer, "console-api-log-event", false); + Services.obs.addObserver(Observer, "http-on-examine-response", false); + registerCleanupFunction(do_cleanup); + + let which = "allow_display"; + + SetupPrefTestEnvironment(which); + + for (let server of Object.keys(test_servers)) { + yield execute_test(server, test_settings[which].mimetype); + } + + SpecialPowers.popPrefEnv(); +}); diff --git a/dom/security/test/hsts/browser_hsts-priming_block_active.js b/dom/security/test/hsts/browser_hsts-priming_block_active.js new file mode 100644 index 000000000..a5478b185 --- /dev/null +++ b/dom/security/test/hsts/browser_hsts-priming_block_active.js @@ -0,0 +1,24 @@ +/* + * Description of the test: + * Check that HSTS priming occurs correctly with mixed content when active + * content is blocked. + */ +'use strict'; + +//jscs:disable +add_task(function*() { + //jscs:enable + Services.obs.addObserver(Observer, "console-api-log-event", false); + Services.obs.addObserver(Observer, "http-on-examine-response", false); + registerCleanupFunction(do_cleanup); + + let which = "block_active"; + + SetupPrefTestEnvironment(which); + + for (let server of Object.keys(test_servers)) { + yield execute_test(server, test_settings[which].mimetype); + } + + SpecialPowers.popPrefEnv(); +}); diff --git a/dom/security/test/hsts/browser_hsts-priming_block_active_css.js b/dom/security/test/hsts/browser_hsts-priming_block_active_css.js new file mode 100644 index 000000000..340d11483 --- /dev/null +++ b/dom/security/test/hsts/browser_hsts-priming_block_active_css.js @@ -0,0 +1,24 @@ +/* + * Description of the test: + * Check that HSTS priming occurs correctly with mixed content when active + * content is blocked for css. + */ +'use strict'; + +//jscs:disable +add_task(function*() { + //jscs:enable + Services.obs.addObserver(Observer, "console-api-log-event", false); + Services.obs.addObserver(Observer, "http-on-examine-response", false); + registerCleanupFunction(do_cleanup); + + let which = "block_active_css"; + + SetupPrefTestEnvironment(which); + + for (let server of Object.keys(test_servers)) { + yield execute_test(server, test_settings[which].mimetype); + } + + SpecialPowers.popPrefEnv(); +}); diff --git a/dom/security/test/hsts/browser_hsts-priming_block_active_with_redir_same.js b/dom/security/test/hsts/browser_hsts-priming_block_active_with_redir_same.js new file mode 100644 index 000000000..130a3d5ec --- /dev/null +++ b/dom/security/test/hsts/browser_hsts-priming_block_active_with_redir_same.js @@ -0,0 +1,24 @@ +/* + * Description of the test: + * Check that HSTS priming occurs correctly with mixed content when active + * content is blocked and redirect to the same host should still upgrade. + */ +'use strict'; + +//jscs:disable +add_task(function*() { + //jscs:enable + Services.obs.addObserver(Observer, "console-api-log-event", false); + Services.obs.addObserver(Observer, "http-on-examine-response", false); + registerCleanupFunction(do_cleanup); + + let which = "block_active_with_redir_same"; + + SetupPrefTestEnvironment(which); + + for (let server of Object.keys(test_servers)) { + yield execute_test(server, test_settings[which].mimetype); + } + + SpecialPowers.popPrefEnv(); +}); diff --git a/dom/security/test/hsts/browser_hsts-priming_block_display.js b/dom/security/test/hsts/browser_hsts-priming_block_display.js new file mode 100644 index 000000000..4eca62718 --- /dev/null +++ b/dom/security/test/hsts/browser_hsts-priming_block_display.js @@ -0,0 +1,24 @@ +/* + * Description of the test: + * Check that HSTS priming occurs correctly with mixed content when display + * content is blocked. + */ +'use strict'; + +//jscs:disable +add_task(function*() { + //jscs:enable + Services.obs.addObserver(Observer, "console-api-log-event", false); + Services.obs.addObserver(Observer, "http-on-examine-response", false); + registerCleanupFunction(do_cleanup); + + let which = "block_display"; + + SetupPrefTestEnvironment(which); + + for (let server of Object.keys(test_servers)) { + yield execute_test(server, test_settings[which].mimetype); + } + + SpecialPowers.popPrefEnv(); +}); diff --git a/dom/security/test/hsts/browser_hsts-priming_cache-timeout.js b/dom/security/test/hsts/browser_hsts-priming_cache-timeout.js new file mode 100644 index 000000000..5416a71d2 --- /dev/null +++ b/dom/security/test/hsts/browser_hsts-priming_cache-timeout.js @@ -0,0 +1,36 @@ +/* + * Description of the test: + * Test that the network.hsts_priming.cache_timeout preferene causes the cache + * to timeout + */ +'use strict'; + +//jscs:disable +add_task(function*() { + //jscs:enable + Observer.add_observers(Services); + registerCleanupFunction(do_cleanup); + + let which = "block_display"; + + SetupPrefTestEnvironment(which, [["security.mixed_content.hsts_priming_cache_timeout", 1]]); + + yield execute_test("no-ssl", test_settings[which].mimetype); + + let pre_promise = performance.now(); + + while ((performance.now() - pre_promise) < 2000) { + yield new Promise(function (resolve) { + setTimeout(resolve, 2000); + }); + } + + // clear the fact that we saw a priming request + test_settings[which].priming = {}; + + yield execute_test("no-ssl", test_settings[which].mimetype); + is(test_settings[which].priming["no-ssl"], true, + "Correctly send a priming request after expiration."); + + SpecialPowers.popPrefEnv(); +}); diff --git a/dom/security/test/hsts/browser_hsts-priming_hsts_after_mixed.js b/dom/security/test/hsts/browser_hsts-priming_hsts_after_mixed.js new file mode 100644 index 000000000..89ea6fbeb --- /dev/null +++ b/dom/security/test/hsts/browser_hsts-priming_hsts_after_mixed.js @@ -0,0 +1,24 @@ +/* + * Description of the test: + * Check that HSTS priming occurs correctly with mixed content when the + * mixed-content blocks before HSTS. + */ +'use strict'; + +//jscs:disable +add_task(function*() { + //jscs:enable + Services.obs.addObserver(Observer, "console-api-log-event", false); + Services.obs.addObserver(Observer, "http-on-examine-response", false); + registerCleanupFunction(do_cleanup); + + let which = "hsts_after_mixed"; + + SetupPrefTestEnvironment(which); + + for (let server of Object.keys(test_servers)) { + yield execute_test(server, test_settings[which].mimetype); + } + + SpecialPowers.popPrefEnv(); +}); diff --git a/dom/security/test/hsts/browser_hsts-priming_no-duplicates.js b/dom/security/test/hsts/browser_hsts-priming_no-duplicates.js new file mode 100644 index 000000000..3846fe4f0 --- /dev/null +++ b/dom/security/test/hsts/browser_hsts-priming_no-duplicates.js @@ -0,0 +1,30 @@ +/* + * Description of the test: + * Only one request should be sent per host, even if we run the test more + * than once. + */ +'use strict'; + +//jscs:disable +add_task(function*() { + //jscs:enable + Observer.add_observers(Services); + registerCleanupFunction(do_cleanup); + + let which = "block_display"; + + SetupPrefTestEnvironment(which); + + for (let server of Object.keys(test_servers)) { + yield execute_test(server, test_settings[which].mimetype); + } + + test_settings[which].priming = {}; + + // run the tests twice to validate the cache is being used + for (let server of Object.keys(test_servers)) { + yield execute_test(server, test_settings[which].mimetype); + } + + SpecialPowers.popPrefEnv(); +}); diff --git a/dom/security/test/hsts/file_1x1.png b/dom/security/test/hsts/file_1x1.png Binary files differnew file mode 100644 index 000000000..1ba31ba1a --- /dev/null +++ b/dom/security/test/hsts/file_1x1.png diff --git a/dom/security/test/hsts/file_priming-top.html b/dom/security/test/hsts/file_priming-top.html new file mode 100644 index 000000000..b1d1bfa40 --- /dev/null +++ b/dom/security/test/hsts/file_priming-top.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1246540</title> + <meta http-equiv='content-type' content="text/html;charset=utf-8" /> +</head> +<body> + <p id="display"></p> + <div id="content" style="visibility: hidden"> + </div> + +<script type="text/javascript"> +/* + * Description of the test: + * Attempt to load an insecure resource. If the resource responds to HSTS + * priming with an STS header, the load should continue securely. + * If it does not, the load should continue be blocked or continue insecurely. + */ + +function parse_query_string() { + var q = {}; + document.location.search.substr(1). + split('&').forEach(function (item, idx, ar) { + let [k, v] = item.split('='); + q[k] = unescape(v); + }); + return q; +} + +var args = parse_query_string(); + +var subresources = { + css: { mimetype: 'text/css', file: 'file_stylesheet.css' }, + img: { mimetype: 'image/png', file: 'file_1x1.png' }, + script: { mimetype: 'text/javascript', file: 'file_priming.js' }, +}; + +function handler(ev) { + console.log("HSTS_PRIMING: Blocked "+args.id); +} + +function loadCss(src) { + let head = document.getElementsByTagName("head")[0]; + let link = document.createElement("link"); + link.setAttribute("rel", "stylesheet"); + link.setAttribute("type", subresources[args.type].mimetype); + link.setAttribute("href", src); + head.appendChild(link); +} + +function loadResource(src) { + let content = document.getElementById("content"); + let testElem = document.createElement(args.type); + testElem.setAttribute("id", args.id); + testElem.setAttribute("charset", "UTF-8"); + testElem.onerror = handler; + content.appendChild(testElem); + testElem.src = src; +} + +function loadTest() { + let subresource = subresources[args.type]; + + let src = "http://" + + args.host + + "/browser/dom/security/test/hsts/file_testserver.sjs" + + "?file=" +escape("browser/dom/security/test/hsts/" + subresource.file) + + "&primer=" + escape(args.id) + + "&mimetype=" + escape(subresource.mimetype) + ; + if (args.type == 'css') { + loadCss(src); + return; + } + + loadResource(src); +} + +// start running the tests +loadTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/hsts/file_priming.js b/dom/security/test/hsts/file_priming.js new file mode 100644 index 000000000..023022da6 --- /dev/null +++ b/dom/security/test/hsts/file_priming.js @@ -0,0 +1,4 @@ +function completed() { + return; +} +completed(); diff --git a/dom/security/test/hsts/file_stylesheet.css b/dom/security/test/hsts/file_stylesheet.css new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/dom/security/test/hsts/file_stylesheet.css diff --git a/dom/security/test/hsts/file_testserver.sjs b/dom/security/test/hsts/file_testserver.sjs new file mode 100644 index 000000000..d5cd6b17a --- /dev/null +++ b/dom/security/test/hsts/file_testserver.sjs @@ -0,0 +1,66 @@ +// SJS file for HSTS mochitests + +Components.utils.import("resource://gre/modules/NetUtil.jsm"); +Components.utils.importGlobalProperties(["URLSearchParams"]); + +function loadFromFile(path) { + // Load the HTML to return in the response from file. + // Since it's relative to the cwd of the test runner, we start there and + // append to get to the actual path of the file. + var testFile = + Components.classes["@mozilla.org/file/directory_service;1"]. + getService(Components.interfaces.nsIProperties). + get("CurWorkD", Components.interfaces.nsILocalFile); + var dirs = path.split("/"); + for (var i = 0; i < dirs.length; i++) { + testFile.append(dirs[i]); + } + var testFileStream = + Components.classes["@mozilla.org/network/file-input-stream;1"]. + createInstance(Components.interfaces.nsIFileInputStream); + testFileStream.init(testFile, -1, 0, 0); + var test = NetUtil.readInputStreamToString(testFileStream, testFileStream.available()); + return test; +} + +function handleRequest(request, response) +{ + const query = new URLSearchParams(request.queryString); + + redir = query.get('redir'); + if (redir == 'same') { + query.delete("redir"); + response.setStatus(302); + let newURI = request.uri; + newURI.queryString = query.serialize(); + response.setHeader("Location", newURI.spec) + } + + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // if we have a priming header, check for required behavior + // and set header appropriately + if (request.hasHeader('Upgrade-Insecure-Requests')) { + var expected = query.get('primer'); + if (expected == 'prime-hsts') { + // set it for 5 minutes + response.setHeader("Strict-Transport-Security", "max-age="+(60*5), false); + } else if (expected == 'reject-upgrade') { + response.setHeader("Strict-Transport-Security", "max-age=0", false); + } + response.write(''); + return; + } + + var file = query.get('file'); + if (file) { + var mimetype = unescape(query.get('mimetype')); + response.setHeader("Content-Type", mimetype, false); + response.write(loadFromFile(unescape(file))); + return; + } + + response.setHeader("Content-Type", "application/json", false); + response.write('{}'); +} diff --git a/dom/security/test/hsts/head.js b/dom/security/test/hsts/head.js new file mode 100644 index 000000000..362b36444 --- /dev/null +++ b/dom/security/test/hsts/head.js @@ -0,0 +1,308 @@ +/* + * Description of the tests: + * Check that HSTS priming occurs correctly with mixed content + * + * This test uses three hostnames, each of which treats an HSTS priming + * request differently. + * * no-ssl never returns an ssl response + * * reject-upgrade returns an ssl response, but with no STS header + * * prime-hsts returns an ssl response with the appropriate STS header + * + * For each server, test that it response appropriately when the we allow + * or block active or display content, as well as when we send an hsts priming + * request, but do not change the order of mixed-content and HSTS. + * + * Test use http-on-examine-response, so must be run in browser context. + */ +'use strict'; + +var TOP_URI = "https://example.com/browser/dom/security/test/hsts/file_priming-top.html"; + +var test_servers = { + // a test server that does not support TLS + 'no-ssl': { + host: 'example.co.jp', + response: false, + id: 'no-ssl', + }, + // a test server which does not support STS upgrade + 'reject-upgrade': { + host: 'example.org', + response: true, + id: 'reject-upgrade', + }, + // a test server when sends an STS header when priming + 'prime-hsts': { + host: 'test1.example.com', + response: true, + id: 'prime-hsts' + }, +}; + +var test_settings = { + // mixed active content is allowed, priming will upgrade + allow_active: { + block_active: false, + block_display: false, + use_hsts: true, + send_hsts_priming: true, + type: 'script', + result: { + 'no-ssl': 'insecure', + 'reject-upgrade': 'insecure', + 'prime-hsts': 'secure', + }, + }, + // mixed active content is blocked, priming will upgrade + block_active: { + block_active: true, + block_display: false, + use_hsts: true, + send_hsts_priming: true, + type: 'script', + result: { + 'no-ssl': 'blocked', + 'reject-upgrade': 'blocked', + 'prime-hsts': 'secure', + }, + }, + // keep the original order of mixed-content and HSTS, but send + // priming requests + hsts_after_mixed: { + block_active: true, + block_display: false, + use_hsts: false, + send_hsts_priming: true, + type: 'script', + result: { + 'no-ssl': 'blocked', + 'reject-upgrade': 'blocked', + 'prime-hsts': 'blocked', + }, + }, + // mixed display content is allowed, priming will upgrade + allow_display: { + block_active: true, + block_display: false, + use_hsts: true, + send_hsts_priming: true, + type: 'img', + result: { + 'no-ssl': 'insecure', + 'reject-upgrade': 'insecure', + 'prime-hsts': 'secure', + }, + }, + // mixed display content is blocked, priming will upgrade + block_display: { + block_active: true, + block_display: true, + use_hsts: true, + send_hsts_priming: true, + type: 'img', + result: { + 'no-ssl': 'blocked', + 'reject-upgrade': 'blocked', + 'prime-hsts': 'secure', + }, + }, + // mixed active content is blocked, priming will upgrade (css) + block_active_css: { + block_active: true, + block_display: false, + use_hsts: true, + send_hsts_priming: true, + type: 'css', + result: { + 'no-ssl': 'blocked', + 'reject-upgrade': 'blocked', + 'prime-hsts': 'secure', + }, + }, + // mixed active content is blocked, priming will upgrade + // redirect to the same host + block_active_with_redir_same: { + block_active: true, + block_display: false, + use_hsts: true, + send_hsts_priming: true, + type: 'script', + redir: 'same', + result: { + 'no-ssl': 'blocked', + 'reject-upgrade': 'blocked', + 'prime-hsts': 'secure', + }, + }, +} +// track which test we are on +var which_test = ""; + +const Observer = { + observe: function (subject, topic, data) { + switch (topic) { + case 'console-api-log-event': + return Observer.console_api_log_event(subject, topic, data); + case 'http-on-examine-response': + return Observer.http_on_examine_response(subject, topic, data); + case 'http-on-modify-request': + return Observer.http_on_modify_request(subject, topic, data); + } + throw "Can't handle topic "+topic; + }, + add_observers: function (services) { + services.obs.addObserver(Observer, "console-api-log-event", false); + services.obs.addObserver(Observer, "http-on-examine-response", false); + services.obs.addObserver(Observer, "http-on-modify-request", false); + }, + // When a load is blocked which results in an error event within a page, the + // test logs to the console. + console_api_log_event: function (subject, topic, data) { + var message = subject.wrappedJSObject.arguments[0]; + // when we are blocked, this will match the message we sent to the console, + // ignore everything else. + var re = RegExp(/^HSTS_PRIMING: Blocked ([-\w]+).*$/); + if (!re.test(message)) { + return; + } + + let id = message.replace(re, '$1'); + let curTest =test_servers[id]; + + if (!curTest) { + ok(false, "HSTS priming got a console message blocked, "+ + "but doesn't match expectations "+id+" (msg="+message); + return; + } + + is("blocked", test_settings[which_test].result[curTest.id], "HSTS priming "+ + which_test+":"+curTest.id+" expected "+ + test_settings[which_test].result[curTest.id]+", got blocked"); + test_settings[which_test].finished[curTest.id] = "blocked"; + }, + get_current_test: function(uri) { + for (let item in test_servers) { + let re = RegExp('https?://'+test_servers[item].host); + if (re.test(uri)) { + return test_servers[item]; + } + } + return null; + }, + http_on_modify_request: function (subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (channel.requestMethod != 'HEAD') { + return; + } + + let curTest = this.get_current_test(channel.URI.asciiSpec); + + if (!curTest) { + return; + } + + ok(!(curTest.id in test_settings[which_test].priming), "Already saw a priming request for " + curTest.id); + test_settings[which_test].priming[curTest.id] = true; + }, + // When we see a response come back, peek at the response and test it is secure + // or insecure as needed. Addtionally, watch the response for priming requests. + http_on_examine_response: function (subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + let curTest = this.get_current_test(channel.URI.asciiSpec); + + if (!curTest) { + return; + } + + let result = (channel.URI.asciiSpec.startsWith('https:')) ? "secure" : "insecure"; + + // This is a priming request, go ahead and validate we were supposed to see + // a response from the server + if (channel.requestMethod == 'HEAD') { + is(true, curTest.response, "HSTS priming response found " + curTest.id); + return; + } + + // This is the response to our query, make sure it matches + is(result, test_settings[which_test].result[curTest.id], + "HSTS priming result " + which_test + ":" + curTest.id); + test_settings[which_test].finished[curTest.id] = result; + }, +}; + +// opens `uri' in a new tab and focuses it. +// returns the newly opened tab +function openTab(uri) { + let tab = gBrowser.addTab(uri); + + // select tab and make sure its browser is focused + gBrowser.selectedTab = tab; + tab.ownerDocument.defaultView.focus(); + + return tab; +} + +function clear_sts_data() { + for (let test in test_servers) { + SpecialPowers.cleanUpSTSData('http://'+test_servers[test].host); + } +} + +function do_cleanup() { + clear_sts_data(); + + Services.obs.removeObserver(Observer, "console-api-log-event"); + Services.obs.removeObserver(Observer, "http-on-examine-response"); +} + +function SetupPrefTestEnvironment(which, additional_prefs) { + which_test = which; + clear_sts_data(); + + var settings = test_settings[which]; + // priming counts how many priming requests we saw + settings.priming = {}; + // priming counts how many tests were finished + settings.finished= {}; + + var prefs = [["security.mixed_content.block_active_content", + settings.block_active], + ["security.mixed_content.block_display_content", + settings.block_display], + ["security.mixed_content.use_hsts", + settings.use_hsts], + ["security.mixed_content.send_hsts_priming", + settings.send_hsts_priming]]; + + if (additional_prefs) { + for (let idx in additional_prefs) { + prefs.push(additional_prefs[idx]); + } + } + + console.log("prefs=%s", prefs); + + SpecialPowers.pushPrefEnv({'set': prefs}); +} + +// make the top-level test uri +function build_test_uri(base_uri, host, test_id, type) { + return base_uri + + "?host=" + escape(host) + + "&id=" + escape(test_id) + + "&type=" + escape(type); +} + +// open a new tab, load the test, and wait for it to finish +function execute_test(test, mimetype) { + var src = build_test_uri(TOP_URI, test_servers[test].host, + test, test_settings[which_test].type); + + let tab = openTab(src); + test_servers[test]['tab'] = tab; + + let browser = gBrowser.getBrowserForTab(tab); + yield BrowserTestUtils.browserLoaded(browser); + + yield BrowserTestUtils.removeTab(tab); +} diff --git a/dom/security/test/mixedcontentblocker/file_bug803225_test_mailto.html b/dom/security/test/mixedcontentblocker/file_bug803225_test_mailto.html new file mode 100644 index 000000000..f1459d366 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_bug803225_test_mailto.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<!-- +Tests for Mixed Content Blocker - Mailto Protocol Compose Page +https://bugzilla.mozilla.org/show_bug.cgi?id=803225 +--> +<head> <meta charset="utf-8"> +</head> +<body> +Hello +<script>window.close();</script> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/file_frameNavigation.html b/dom/security/test/mixedcontentblocker/file_frameNavigation.html new file mode 100644 index 000000000..fd9ea2317 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_frameNavigation.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<!-- +Tests for Mixed Content Blocker related to navigating children, grandchildren, etc +https://bugzilla.mozilla.org/show_bug.cgi?id=840388 +--> +<head> + <meta charset="utf-8"> + <title>Tests for Mixed Content Frame Navigation</title> +</head> +<body> +<div id="testContent"></div> + +<script> + var baseUrlHttps = "https://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html"; + + // For tests that require setTimeout, set the maximum polling time to 50 x 100ms = 5 seconds. + var MAX_COUNT = 50; + var TIMEOUT_INTERVAL = 100; + + var testContent = document.getElementById("testContent"); + + // Test 1: Navigate secure iframe to insecure iframe on an insecure page + var iframe_test1 = document.createElement("iframe"); + var counter_test1 = 0; + iframe_test1.src = baseUrlHttps + "?insecurePage_navigate_child"; + iframe_test1.setAttribute("id", "test1"); + iframe_test1.onerror = function() { + parent.postMessage({"test": "insecurePage_navigate_child", "msg": "got an onerror alert when loading or navigating testing iframe"}, "http://mochi.test:8888"); + }; + testContent.appendChild(iframe_test1); + + function navigationStatus(iframe_test1) + { + // When the page is navigating, it goes through about:blank and we will get a permission denied for loc. + // Catch that specific exception and return + try { + var loc = document.getElementById("test1").contentDocument.location; + } catch(e) { + if (e.name === "SecurityError") { + // We received an exception we didn't expect. + throw e; + } + counter_test1++; + return; + } + if (loc == "http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?insecurePage_navigate_child_response") { + return; + } + else { + if(counter_test1 < MAX_COUNT) { + counter_test1++; + setTimeout(navigationStatus, TIMEOUT_INTERVAL, iframe_test1); + } + else { + // After we have called setTimeout the maximum number of times, assume navigating the iframe is blocked + parent.postMessage({"test": "insecurePage_navigate_child", "msg": "navigating to insecure iframe blocked on insecure page"}, "http://mochi.test:8888"); + } + } + } + + setTimeout(navigationStatus, TIMEOUT_INTERVAL, iframe_test1); + + // Test 2: Navigate secure grandchild iframe to insecure grandchild iframe on a page that has no secure parents + var iframe_test2 = document.createElement("iframe"); + iframe_test2.src = "http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_grandchild.html" + iframe_test2.onerror = function() { + parent.postMessage({"test": "insecurePage_navigate_grandchild", "msg": "got an on error alert when loading or navigating testing iframe"}, "http://mochi.test:8888"); + }; + testContent.appendChild(iframe_test2); + +</script> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/file_frameNavigation_blankTarget.html b/dom/security/test/mixedcontentblocker/file_frameNavigation_blankTarget.html new file mode 100644 index 000000000..5cfd95984 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_frameNavigation_blankTarget.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<!-- +Tests for Mixed Content Blocker - Opening link with _blank target in an https iframe. +https://bugzilla.mozilla.org/show_bug.cgi?id=841850 +--> +<head> + <meta charset="utf-8"> + <title>Tests for Mixed Content Frame Navigation</title> +</head> +<body> +<a href="http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?blankTarget" id="blankTarget" target="_blank">Go to http site</a> + +<script> + var blankTarget = document.getElementById("blankTarget"); + blankTarget.click(); + + var os = SpecialPowers.Cc["@mozilla.org/observer-service;1"]. + getService(SpecialPowers.Components.interfaces.nsIObserverService); + var observer = { + observe: function(subject, topic, data) { + if(topic == "content-document-global-created" && data =="http://example.com") { + parent.parent.postMessage({"test": "blankTarget", "msg": "opened an http link with target=_blank from a secure page"}, "http://mochi.test:8888"); + os.removeObserver(observer, "content-document-global-created"); + } + } + } + os.addObserver(observer, "content-document-global-created", false); + +</script> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/file_frameNavigation_grandchild.html b/dom/security/test/mixedcontentblocker/file_frameNavigation_grandchild.html new file mode 100644 index 000000000..1034991da --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_frameNavigation_grandchild.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +Tests for Mixed Content Blocker - Navigating Grandchild frames when a secure parent doesn't exist +https://bugzilla.mozilla.org/show_bug.cgi?id=840388 +--> +<head> + <meta charset="utf-8"> + <title>Tests for Mixed Content Frame Navigation</title> +</head> +<body> +<iframe src="https://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?insecurePage_navigate_grandchild" id="child"></iframe> + +<script> + // For tests that require setTimeout, set the maximum polling time to 100 x 100ms = 10 seconds. + var MAX_COUNT = 50; + var TIMEOUT_INTERVAL = 100; + var counter = 0; + + var child = document.getElementById("child"); + function navigationStatus(child) + { + // When the page is navigating, it goes through about:blank and we will get a permission denied for loc. + // Catch that specific exception and return + try { + var loc; + if (child.contentDocument) { + loc = child.contentDocument.location; + } + } catch(e) { + if (e.message && e.message.indexOf("Permission denied to access property") == -1) { + // We received an exception we didn't expect. + throw e; + } + counter++; + return; + } + if (loc == "http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?insecurePage_navigate_grandchild_response") { + return; + } + else { + if(counter < MAX_COUNT) { + counter++; + setTimeout(navigationStatus, TIMEOUT_INTERVAL, child); + } + else { + // After we have called setTimeout the maximum number of times, assume navigating the iframe is blocked + parent.parent.postMessage({"test": "insecurePage_navigate_grandchild", "msg": "navigating to insecure grandchild iframe blocked on insecure page"}, "http://mochi.test:8888"); + } + } + } + + setTimeout(navigationStatus, TIMEOUT_INTERVAL, child); + +</script> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html b/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html new file mode 100644 index 000000000..62b24f37d --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<body> +<div id="content"></div> +<script> + // get the case from the query string + var type = location.search.substring(1); + + switch (type) { + case "insecurePage_navigate_child": + document.getElementById("content").innerHTML = + '<a href="http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?insecurePage_navigate_child_response" id="link">Testing\<\/a>'; + document.getElementById("link").click(); + break; + + case "insecurePage_navigate_child_response": + parent.parent.postMessage({"test": "insecurePage_navigate_child", "msg": "navigated to insecure iframe on insecure page"}, "http://mochi.test:8888"); + document.getElementById("content").innerHTML = "Navigated from secure to insecure frame on an insecure page"; + break; + + case "insecurePage_navigate_grandchild": + document.getElementById("content").innerHTML = + '<a href="http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?insecurePage_navigate_grandchild_response" id="link">Testing\<\/a>'; + // If we don't reflow before clicking the link, the test will fail intermittently. The reason is still unknown. We'll track this issue in bug 1259715. + requestAnimationFrame(function() { + setTimeout(function() { + document.getElementById("link").click(); + }, 0); + }); + break; + + case "insecurePage_navigate_grandchild_response": + parent.parent.parent.postMessage({"test": "insecurePage_navigate_grandchild", "msg": "navigated to insecure grandchild iframe on insecure page"}, "http://mochi.test:8888"); + document.getElementById("content").innerHTML = "Navigated from secure to insecure grandchild frame on an insecure page"; + break; + + case "securePage_navigate_child": + document.getElementById("content").innerHTML = + '<a href="http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?securePage_navigate_child_response" id="link">Testing\<\/a>'; + document.getElementById("link").click(); + break; + + case "securePage_navigate_child_response": + document.getElementById("content").innerHTML = "<p>Navigated from secure to insecure frame on a secure page</p>"; + parent.parent.postMessage({"test": "securePage_navigate_child", "msg": "navigated to insecure iframe on secure page"}, "http://mochi.test:8888"); + break; + + case "securePage_navigate_grandchild": + document.getElementById("content").innerHTML= + '<a href="http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?securePage_navigate_grandchild_response" id="link">Testing\<\/a>'; + document.getElementById("link").click(); + break; + + case "securePage_navigate_grandchild_response": + dump("\nNavigated to grandchild iframe from secure location to insecure location. About to post message to the top page.\n"); + parent.parent.parent.postMessage({"test": "securePage_navigate_grandchild", "msg": "navigated to insecure grandchild iframe on secure page"}, "http://mochi.test:8888"); + dump("\npostMessage to parent attempted.\n"); + document.getElementById("content").innerHTML = "<p>Navigated from secure to insecure grandchild frame on a secure page</p>"; + break; + + case "blankTarget": + window.close(); + break; + + default: + document.getElementById("content").innerHTML = "Hello"; + break; + } + +</script> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/file_frameNavigation_secure.html b/dom/security/test/mixedcontentblocker/file_frameNavigation_secure.html new file mode 100644 index 000000000..ea8462c39 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_frameNavigation_secure.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<!-- +Tests for Mixed Content Blocker related to navigating children, grandchildren, etc +https://bugzilla.mozilla.org/show_bug.cgi?id=840388 +--> +<head> + <meta charset="utf-8"> + <title>Tests for Mixed Content Frame Navigation</title> +</head> +<body> +<div id="testContent"></div> + +<script> + var baseUrl = "https://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html"; + + // For tests that require setTimeout, set the maximum polling time to 50 x 100ms = 5 seconds. + var MAX_COUNT = 50; + var TIMEOUT_INTERVAL = 100; + + var testContent = document.getElementById("testContent"); + + // Test 1: Navigate secure iframe to insecure iframe on a secure page + var iframe_test1 = document.createElement("iframe"); + var counter_test1 = 0; + iframe_test1.setAttribute("id", "test1"); + iframe_test1.src = baseUrl + "?securePage_navigate_child"; + iframe_test1.onerror = function() { + parent.postMessage({"test": "securePage_navigate_child", "msg": "got an onerror event when loading or navigating testing iframe"}, "http://mochi.test:8888"); + }; + testContent.appendChild(iframe_test1); + + function navigationStatus(iframe_test1) + { + // When the page is navigating, it goes through about:blank and we will get a permission denied for loc. + // Catch that specific exception and return + try { + var loc = document.getElementById("test1").contentDocument.location; + } catch(e) { + if (e.name === "SecurityError") { + // We received an exception we didn't expect. + throw e; + } + counter_test1++; + return; + } + if (loc == "http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?insecurePage_navigate_child_response") { + return; + } else { + if(counter_test1 < MAX_COUNT) { + counter_test1++; + setTimeout(navigationStatus, TIMEOUT_INTERVAL, iframe_test1); + } + else { + // After we have called setTimeout the maximum number of times, assume navigating the iframe is blocked + parent.postMessage({"test": "securePage_navigate_child", "msg": "navigating to insecure iframe blocked on secure page"}, "http://mochi.test:8888"); + } + } + } + + setTimeout(navigationStatus, TIMEOUT_INTERVAL, iframe_test1); + + // Test 2: Open an http page in a new tab from a link click with target=_blank. + var iframe_test3 = document.createElement("iframe"); + iframe_test3.src = "https://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_blankTarget.html"; + iframe_test3.onerror = function() { + parent.postMessage({"test": "blankTarget", "msg": "got an onerror event when loading or navigating testing iframe"}, "http://mochi.test:8888"); + }; + testContent.appendChild(iframe_test3); + +</script> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/file_frameNavigation_secure_grandchild.html b/dom/security/test/mixedcontentblocker/file_frameNavigation_secure_grandchild.html new file mode 100644 index 000000000..f7f3c4086 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_frameNavigation_secure_grandchild.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +Tests for Mixed Content Blocker - Navigating Grandchild Frames when a secure parent exists +https://bugzilla.mozilla.org/show_bug.cgi?id=840388 +--> +<head> + <meta charset="utf-8"> + <title>Tests for Mixed Content Frame Navigation</title> +</head> +<body> + +<iframe src="https://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?securePage_navigate_grandchild" id="child"></iframe> +<script> + // For tests that require setTimeout, set the maximum polling time to 50 x 100ms = 5 seconds. + var MAX_COUNT = 50; + var TIMEOUT_INTERVAL = 100; + var counter = 0; + + var child = document.getElementById("child"); + function navigationStatus(child) + { + var loc; + // When the page is navigating, it goes through about:blank and we will get a permission denied for loc. + // Catch that specific exception and return + try { + loc = document.getElementById("child").contentDocument.location; + } catch(e) { + if (e.message && e.message.indexOf("Permission denied to access property") == -1) { + // We received an exception we didn't expect. + throw e; + } + counter++; + return; + } + if (loc == "http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?securePage_navigate_grandchild_response") { + return; + } + else { + if(counter < MAX_COUNT) { + counter++; + setTimeout(navigationStatus, TIMEOUT_INTERVAL, child); + } + else { + // After we have called setTimeout the maximum number of times, assume navigating the iframe is blocked + dump("\nThe current location of the grandchild iframe is: "+loc+".\n"); + dump("\nWe have past the maximum timeout. Navigating a grandchild iframe from an https location to an http location on a secure page failed. We are about to post message to the top level page\n"); + parent.parent.postMessage({"test": "securePage_navigate_grandchild", "msg": "navigating to insecure grandchild iframe blocked on secure page"}, "http://mochi.test:8888"); + dump("\nAttempted postMessage\n"); + } + } + } + + setTimeout(navigationStatus, TIMEOUT_INTERVAL, child); + +</script> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/file_main.html b/dom/security/test/mixedcontentblocker/file_main.html new file mode 100644 index 000000000..ade5eefdb --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_main.html @@ -0,0 +1,261 @@ +<!DOCTYPE HTML> +<html> +<!-- +Tests for Mixed Content Blocker +https://bugzilla.mozilla.org/show_bug.cgi?id=62178 +--> +<head> + <meta charset="utf-8"> + <title>Tests for Bug 62178</title> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<div id="testContent"></div> + +<!-- types the Mixed Content Blocker can block + /* + switch (aContentType) { + case nsIContentPolicy::TYPE_OBJECT: + case nsIContentPolicy::TYPE_SCRIPT: + case nsIContentPolicy::TYPE_STYLESHEET: + case nsIContentPolicy::TYPE_SUBDOCUMENT: + case nsIContentPolicy::TYPE_XMLHTTPREQUEST: + + case nsIContentPolicy::TYPE_FONT: - NO TEST: + Load events for external fonts are not detectable by javascript. + case nsIContentPolicy::TYPE_WEBSOCKET: - NO TEST: + websocket connections over https require an encrypted websocket protocol (wss:) + + case nsIContentPolicy::TYPE_IMAGE: + case nsIContentPolicy::TYPE_IMAGESET: + case nsIContentPolicy::TYPE_MEDIA: + case nsIContentPolicy::TYPE_PING: + our ping implementation is off by default and does not comply with the current spec (bug 786347) + case nsIContentPolicy::TYPE_BEACON: + + } + */ +--> + +<script> + var baseUrl = "http://example.com/tests/dom/security/test/mixedcontentblocker/file_server.sjs"; + + //For tests that require setTimeout, set the maximum polling time to 100 x 100ms = 10 seconds. + var MAX_COUNT = 100; + var TIMEOUT_INTERVAL = 100; + + var testContent = document.getElementById("testContent"); + + /* Part 1: Mixed Script tests */ + + // Test 1a: insecure object + var object = document.createElement("object"); + object.data = baseUrl + "?type=object"; + object.type = "application/x-test"; + object.width = "200"; + object.height = "200"; + + testContent.appendChild(object); + + var objectCount = 0; + + function objectStatus(object) { + // Expose our privileged bits on the object + object = SpecialPowers.wrap(object); + + if (object.displayedType != SpecialPowers.Ci.nsIObjectLoadingContent.TYPE_NULL) { + //object loaded + parent.postMessage({"test": "object", "msg": "insecure object loaded"}, "http://mochi.test:8888"); + } + else { + if(objectCount < MAX_COUNT) { + objectCount++; + setTimeout(objectStatus, TIMEOUT_INTERVAL, object); + } + else { + //After we have called setTimeout the maximum number of times, assume object is blocked + parent.postMessage({"test": "object", "msg": "insecure object blocked"}, "http://mochi.test:8888"); + } + } + } + + // object does not have onload and onerror events. Hence we need a setTimeout to check the object's status + setTimeout(objectStatus, TIMEOUT_INTERVAL, object); + + // Test 1b: insecure script + var script = document.createElement("script"); + var scriptLoad = false; + var scriptCount = 0; + script.src = baseUrl + "?type=script"; + script.onload = function() { + parent.postMessage({"test": "script", "msg": "insecure script loaded"}, "http://mochi.test:8888"); + scriptLoad = true; + } + testContent.appendChild(script); + + function scriptStatus(script) + { + if(scriptLoad) { + return; + } + else { + if(scriptCount < MAX_COUNT) { + scriptCount++; + setTimeout(scriptStatus, TIMEOUT_INTERVAL, script); + } + else { + //After we have called setTimeout the maximum number of times, assume script is blocked + parent.postMessage({"test": "script", "msg": "insecure script blocked"}, "http://mochi.test:8888"); + } + } + } + + // scripts blocked by Content Policy's do not have onerror events (see bug 789856). Hence we need a setTimeout to check the script's status + setTimeout(scriptStatus, TIMEOUT_INTERVAL, script); + + + // Test 1c: insecure stylesheet + var cssStyleSheet = document.createElement("link"); + cssStyleSheet.rel = "stylesheet"; + cssStyleSheet.href = baseUrl + "?type=stylesheet"; + cssStyleSheet.type = "text/css"; + testContent.appendChild(cssStyleSheet); + + var styleCount = 0; + + function styleStatus(cssStyleSheet) { + if( cssStyleSheet.sheet || cssStyleSheet.styleSheet || cssStyleSheet.innerHTML ) { + parent.postMessage({"test": "stylesheet", "msg": "insecure stylesheet loaded"}, "http://mochi.test:8888"); + } + else { + if(styleCount < MAX_COUNT) { + styleCount++; + setTimeout(styleStatus, TIMEOUT_INTERVAL, cssStyleSheet); + } + else { + //After we have called setTimeout the maximum number of times, assume stylesheet is blocked + parent.postMessage({"test": "stylesheet", "msg": "insecure stylesheet blocked"}, "http://mochi.test:8888"); + } + } + } + + // link does not have onload and onerror events. Hence we need a setTimeout to check the link's status + window.setTimeout(styleStatus, TIMEOUT_INTERVAL, cssStyleSheet); + + // Test 1d: insecure iframe + var iframe = document.createElement("iframe"); + iframe.src = baseUrl + "?type=iframe"; + iframe.onload = function() { + parent.postMessage({"test": "iframe", "msg": "insecure iframe loaded"}, "http://mochi.test:8888"); + } + iframe.onerror = function() { + parent.postMessage({"test": "iframe", "msg": "insecure iframe blocked"}, "http://mochi.test:8888"); + }; + testContent.appendChild(iframe); + + + // Test 1e: insecure xhr + var xhr = new XMLHttpRequest; + try { + xhr.open("GET", baseUrl + "?type=xhr", true); + xhr.send(); + xhr.onloadend = function (oEvent) { + if (xhr.status == 200) { + parent.postMessage({"test": "xhr", "msg": "insecure xhr loaded"}, "http://mochi.test:8888"); + } + else { + parent.postMessage({"test": "xhr", "msg": "insecure xhr blocked"}, "http://mochi.test:8888"); + } + } + } catch(ex) { + parent.postMessage({"test": "xhr", "msg": "insecure xhr blocked"}, "http://mochi.test:8888"); + } + + /* Part 2: Mixed Display tests */ + + // Shorthand for all image test variants + function imgHandlers(img, test) { + img.onload = function () { + parent.postMessage({"test": test, "msg": "insecure image loaded"}, "http://mochi.test:8888"); + } + img.onerror = function() { + parent.postMessage({"test": test, "msg": "insecure image blocked"}, "http://mochi.test:8888"); + } + } + + // Test 2a: insecure image + var img = document.createElement("img"); + img.src = "http://mochi.test:8888/tests/image/test/mochitest/blue.png"; + imgHandlers(img, "image"); + // We don't need to append the image to the document. Doing so causes the image test to run twice. + + // Test 2b: insecure media + var media = document.createElement("video"); + media.src = "http://mochi.test:8888/tests/dom/media/test/320x240.ogv?" + Math.floor((Math.random()*1000)+1); + media.width = "320"; + media.height = "200"; + media.type = "video/ogg"; + media.onloadeddata = function() { + parent.postMessage({"test": "media", "msg": "insecure media loaded"}, "http://mochi.test:8888"); + } + media.onerror = function() { + parent.postMessage({"test": "media", "msg": "insecure media blocked"}, "http://mochi.test:8888"); + } + // We don't need to append the video to the document. Doing so causes the image test to run twice. + + /* Part 3: Mixed Active Tests for Image srcset */ + + // Test 3a: image with srcset + var imgA = document.createElement("img"); + imgA.srcset = "http://mochi.test:8888/tests/image/test/mochitest/blue.png"; + imgHandlers(imgA, "imageSrcset"); + + // Test 3b: image with srcset, using fallback from src, should still use imageset policy + var imgB = document.createElement("img"); + imgB.srcset = " "; + imgB.src = "http://mochi.test:8888/tests/image/test/mochitest/blue.png"; + imgHandlers(imgB, "imageSrcsetFallback"); + + // Test 3c: image in <picture> + var imgC = document.createElement("img"); + var pictureC = document.createElement("picture"); + var sourceC = document.createElement("source"); + sourceC.srcset = "http://mochi.test:8888/tests/image/test/mochitest/blue.png"; + pictureC.appendChild(sourceC); + pictureC.appendChild(imgC); + imgHandlers(imgC, "imagePicture"); + + // Test 3d: Loaded basic image switching to a <picture>, loading + // same source, should still redo the request with new + // policy. + var imgD = document.createElement("img"); + imgD.src = "http://mochi.test:8888/tests/image/test/mochitest/blue.png"; + imgD.onload = imgD.onerror = function() { + // Whether or not it loads, we want to now append it to a picture and observe + var pictureD = document.createElement("picture"); + var sourceD = document.createElement("source"); + sourceD.srcset = "http://mochi.test:8888/tests/image/test/mochitest/blue.png"; + pictureD.appendChild(sourceD); + pictureD.appendChild(imgD); + imgHandlers(imgD, "imageJoinPicture"); + } + + // Test 3e: img load from <picture> source reverts to img.src as it + // is removed -- the new request should revert to mixed + // display policy + var imgE = document.createElement("img"); + var pictureE = document.createElement("picture"); + var sourceE = document.createElement("source"); + sourceE.srcset = "http://mochi.test:8888/tests/image/test/mochitest/blue.png"; + pictureE.appendChild(sourceE); + pictureE.appendChild(imgE); + imgE.src = "http://mochi.test:8888/tests/image/test/mochitest/blue.png"; + imgE.onload = imgE.onerror = function() { + // Whether or not it loads, remove it from the picture and observe + pictureE.removeChild(imgE) + imgHandlers(imgE, "imageLeavePicture"); + } + +</script> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/file_main_bug803225.html b/dom/security/test/mixedcontentblocker/file_main_bug803225.html new file mode 100644 index 000000000..e657be256 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_main_bug803225.html @@ -0,0 +1,182 @@ +<!DOCTYPE HTML> +<html> +<!-- +Tests for Mixed Content Blocker - Allowed Protocols +https://bugzilla.mozilla.org/show_bug.cgi?id=803225 +--> +<head> + <meta charset="utf-8"> + <title>Tests for Bug 62178</title> + <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<div id="testContent"></div> + +<!-- Test additional schemes the Mixed Content Blocker should not block + "about" protocol URIs that are URI_SAFE_FOR_UNTRUSTED_CONTENT (moz-safe-about; see nsAboutProtocolHandler::NewURI + "data", + "javascript", + "mailto", + "resource", + "moz-icon", + "wss" +--> + +<script> + + //For tests that require setTimeout, set the timeout interval + var TIMEOUT_INTERVAL = 100; + + var testContent = document.getElementById("testContent"); + + // Test 1 & 2: about and javascript protcols within an iframe + var data = Array(2,2); + var protocols = [ + ["about", ""], //When no source is specified, the frame gets a source of about:blank + ["javascript", "javascript:document.open();document.write='<h1>SUCCESS</h1>';document.close();"], + ]; + for(var i=0; i < protocols.length; i++) + { + var generic_frame = document.createElement("iframe"); + generic_frame.src = protocols[i][1]; + generic_frame.name="generic_protocol"; + + generic_frame.onload = function(i) { + data = {"test": protocols[i][0], "msg": "resource with " + protocols[i][0] + " protocol loaded"}; + parent.postMessage(data, "http://mochi.test:8888"); + }.bind(generic_frame, i) + + generic_frame.onerror = function(i) { + data = {"test": protocols[i][0], "msg": "resource with " + protocols[i][0] + " protocol did not load"}; + parent.postMessage(data, "http://mochi.test:8888"); + }.bind(generic_frame, i); + + testContent.appendChild(generic_frame, i); + } + + // Test 3: for resource within a script tag + // Note: the script we load throws an exception, but the script element's + // onload listener is called after we successfully fetch the script, + // independently of whether it throws an exception. + var resource_script=document.createElement("script"); + resource_script.src = "resource://gre/modules/XPCOMUtils.jsm"; + resource_script.name = "resource_protocol"; + resource_script.onload = function() { + parent.postMessage({"test": "resource", "msg": "resource with resource protocol loaded"}, "http://mochi.test:8888"); + } + resource_script.onerror = function() { + parent.postMessage({"test": "resource", "msg": "resource with resource protocol did not load"}, "http://mochi.test:8888"); + } + + testContent.appendChild(resource_script); + + // Test 4: moz-icon within an img tag + var image=document.createElement("img"); + image.src = "moz-icon://dummy.exe?size=16"; + image.onload = function() { + parent.postMessage({"test": "mozicon", "msg": "resource with mozicon protocol loaded"}, "http://mochi.test:8888"); + } + image.onerror = function() { + parent.postMessage({"test": "mozicon", "msg": "resource with mozicon protocol did not load"}, "http://mochi.test:8888"); + } + // We don't need to append the image to the document. Doing so causes the image test to run twice. + + // Test 5: about unsafe protocol within an iframe + var unsafe_about_frame = document.createElement("iframe"); + unsafe_about_frame.src = "about:config"; + unsafe_about_frame.name = "unsafe_about_protocol"; + unsafe_about_frame.onload = function() { + parent.postMessage({"test": "unsafe_about", "msg": "resource with unsafe about protocol loaded"}, "http://mochi.test:8888"); + } + unsafe_about_frame.onerror = function() { + parent.postMessage({"test": "unsafe_about", "msg": "resource with unsafe about protocol did not load"}, "http://mochi.test:8888"); + } + testContent.appendChild(unsafe_about_frame); + + // Test 6: data protocol within a script tag + var x = 2; + var newscript = document.createElement("script"); + newscript.src= "data:text/javascript,var x = 4;"; + newscript.onload = function() { + parent.postMessage({"test": "data_protocol", "msg": "resource with data protocol loaded"}, "http://mochi.test:8888"); + } + newscript.onerror = function() { + parent.postMessage({"test": "data_protocol", "msg": "resource with data protocol did not load"}, "http://mochi.test:8888"); + } + testContent.appendChild(newscript); + + // Test 7: mailto protocol + let mm = SpecialPowers.loadChromeScript(function launchHandler() { + var { classes: Cc, interfaces: Ci } = Components; + var ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + + var webHandler = Cc["@mozilla.org/uriloader/web-handler-app;1"]. + createInstance(Ci.nsIWebHandlerApp); + webHandler.name = "Web Handler"; + webHandler.uriTemplate = "http://example.com/tests/dom/security/test/mixedcontentblocker/file_bug803225_test_mailto.html?s=%"; + + Components.utils.import("resource://gre/modules/Services.jsm"); + Services.ppmm.addMessageListener("Test:content-ready", function contentReadyListener() { + Services.ppmm.removeMessageListener("Test:content-ready", contentReadyListener); + sendAsyncMessage("Test:content-ready-forward"); + Services.ppmm.removeDelayedProcessScript(pScript); + }) + + var pScript = "data:,new " + function () { + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + var observer = { + observe: function(subject, topic, data) { + if (topic == "content-document-global-created" && data == "http://example.com") { + sendAsyncMessage("Test:content-ready"); + os.removeObserver(observer, "content-document-global-created"); + } + } + }; + os.addObserver(observer, "content-document-global-created", false); + } + + Services.ppmm.loadProcessScript(pScript, true); + + var uri = ioService.newURI("mailto:foo@bar.com", null, null); + webHandler.launchWithURI(uri); + }); + + var mailto = false; + + mm.addMessageListener("Test:content-ready-forward", function contentReadyListener() { + mm.removeMessageListener("Test:content-ready-forward", contentReadyListener); + mailto = true; + parent.postMessage({"test": "mailto", "msg": "resource with mailto protocol loaded"}, "http://mochi.test:8888"); + }); + + function mailtoProtocolStatus() { + if(!mailto) { + //There is no onerror event associated with the WebHandler, and hence we need a setTimeout to check the status + setTimeout(mailtoProtocolStatus, TIMEOUT_INTERVAL); + } + } + + mailtoProtocolStatus(); + + // Test 8: wss protocol + var wss; + wss = new WebSocket("wss://example.com/tests/dom/security/test/mixedcontentblocker/file_main_bug803225_websocket"); + + var status_wss = "started"; + wss.onopen = function(e) { + status_wss = "opened"; + wss.close(); + } + wss.onclose = function(e) { + if(status_wss == "opened") { + parent.postMessage({"test": "wss", "msg": "resource with wss protocol loaded"}, "http://mochi.test:8888"); + } else { + parent.postMessage({"test": "wss", "msg": "resource with wss protocol did not load"}, "http://mochi.test:8888"); + } + } + +</script> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/file_main_bug803225_websocket_wsh.py b/dom/security/test/mixedcontentblocker/file_main_bug803225_websocket_wsh.py new file mode 100644 index 000000000..8c33c6b10 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_main_bug803225_websocket_wsh.py @@ -0,0 +1,7 @@ +from mod_pywebsocket import msgutil + +def web_socket_do_extra_handshake(request): + pass + +def web_socket_transfer_data(request): + resp = "" diff --git a/dom/security/test/mixedcontentblocker/file_server.sjs b/dom/security/test/mixedcontentblocker/file_server.sjs new file mode 100644 index 000000000..58dffc565 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_server.sjs @@ -0,0 +1,45 @@ + +function handleRequest(request, response) +{ + // get the Content-Type to serve from the query string + var contentType = null; + request.queryString.split('&').forEach( function (val) { + var [name, value] = val.split('='); + if (name == "type") { + contentType = unescape(value); + } + }); + + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + switch (contentType) { + case "iframe": + response.setHeader("Content-Type", "text/html", false); + response.write("frame content"); + break; + + case "script": + response.setHeader("Content-Type", "application/javascript", false); + break; + + case "stylesheet": + response.setHeader("Content-Type", "text/css", false); + break; + + case "object": + response.setHeader("Content-Type", "application/x-test", false); + break; + + case "xhr": + response.setHeader("Content-Type", "text/xml", false); + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.write('<?xml version="1.0" encoding="UTF-8" ?><test></test>'); + break; + + default: + response.setHeader("Content-Type", "text/html", false); + response.write("<html><body>Hello World</body></html>"); + break; + } +} diff --git a/dom/security/test/mixedcontentblocker/mochitest.ini b/dom/security/test/mixedcontentblocker/mochitest.ini new file mode 100644 index 000000000..94c17f9b4 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/mochitest.ini @@ -0,0 +1,23 @@ +[DEFAULT] +tags = mcb +support-files = + file_bug803225_test_mailto.html + file_frameNavigation.html + file_frameNavigation_blankTarget.html + file_frameNavigation_grandchild.html + file_frameNavigation_innermost.html + file_frameNavigation_secure.html + file_frameNavigation_secure_grandchild.html + file_main.html + file_main_bug803225.html + file_main_bug803225_websocket_wsh.py + file_server.sjs + !/dom/media/test/320x240.ogv + !/image/test/mochitest/blue.png + +[test_main.html] +skip-if = toolkit == 'android' #TIMED_OUT +[test_bug803225.html] +skip-if = toolkit == 'android' #TIMED_OUT +[test_frameNavigation.html] +skip-if = toolkit == 'android' #TIMED_OUT diff --git a/dom/security/test/mixedcontentblocker/test_bug803225.html b/dom/security/test/mixedcontentblocker/test_bug803225.html new file mode 100644 index 000000000..13c52762d --- /dev/null +++ b/dom/security/test/mixedcontentblocker/test_bug803225.html @@ -0,0 +1,152 @@ +<!DOCTYPE HTML> +<html> +<!-- +Testing Whitelist of Resource Scheme for Mixed Content Blocker +https://bugzilla.mozilla.org/show_bug.cgi?id=803225 +--> +<head> + <meta charset="utf-8"> + <title>Tests for Bug 803225</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script> + var counter = 0; + var settings = [ [true, true], [true, false], [false, true], [false, false] ]; + + var blockActive; + var blockDisplay; + + //Cycle through 4 different preference settings. + function changePrefs(callback) { + let newPrefs = [["security.mixed_content.block_display_content", settings[counter][0]], + ["security.mixed_content.block_active_content", settings[counter][1]]]; + + SpecialPowers.pushPrefEnv({"set": newPrefs}, function () { + blockDisplay = SpecialPowers.getBoolPref("security.mixed_content.block_display_content"); + blockActive = SpecialPowers.getBoolPref("security.mixed_content.block_active_content"); + counter++; + callback(); + }); + } + + var testsToRun = { + /* https - Tests already run as part of bug 62178. */ + about: false, + mozicon: false, + resource: false, + unsafe_about: false, + data_protocol: false, + javascript: false, + mailto: false, + wss: false, + }; + + function log(msg) { + document.getElementById("log").textContent += "\n" + msg; + } + + function reloadFrame() { + document.getElementById('framediv').innerHTML = '<iframe id="testHarness" src="https://example.com/tests/dom/security/test/mixedcontentblocker/file_main_bug803225.html"></iframe>'; + } + + function checkTestsCompleted() { + for (var prop in testsToRun) { + // some test hasn't run yet so we're not done + if (!testsToRun[prop]) + return; + } + //if the testsToRun are all completed, change the pref and run the tests again until we have cycled through all the prefs. + if(counter < 4) { + for (var prop in testsToRun) { + testsToRun[prop] = false; + } + //call to change the preferences + changePrefs(function() { + log("\nblockDisplay set to "+blockDisplay+", blockActive set to "+blockActive+"."); + reloadFrame(); + }); + } + else { + SimpleTest.finish(); + } + } + + var firstTest = true; + + function receiveMessage(event) { + if(firstTest) { + log("blockDisplay set to "+blockDisplay+", blockActive set to "+blockActive+"."); + firstTest = false; + } + + log("test: "+event.data.test+", msg: "+event.data.msg + " logging message."); + // test that the load type matches the pref for this type of content + // (i.e. active vs. display) + + switch(event.data.test) { + + /* Mixed Script tests */ + case "about": + ok(event.data.msg == "resource with about protocol loaded", "resource with about protocol did not load"); + testsToRun["about"] = true; + break; + + case "resource": + ok(event.data.msg == "resource with resource protocol loaded", "resource with resource protocol did not load"); + testsToRun["resource"] = true; + break; + + case "mozicon": + ok(event.data.msg == "resource with mozicon protocol loaded", "resource with mozicon protocol did not load"); + testsToRun["mozicon"] = true; + break; + + case "unsafe_about": + // This one should not load + ok(event.data.msg == "resource with unsafe about protocol did not load", "resource with unsafe about protocol loaded"); + testsToRun["unsafe_about"] = true; + break; + + case "data_protocol": + ok(event.data.msg == "resource with data protocol loaded", "resource with data protocol did not load"); + testsToRun["data_protocol"] = true; + break; + + case "javascript": + ok(event.data.msg == "resource with javascript protocol loaded", "resource with javascript protocol did not load"); + testsToRun["javascript"] = true; + break; + + case "wss": + ok(event.data.msg == "resource with wss protocol loaded", "resource with wss protocol did not load"); + testsToRun["wss"] = true; + break; + + case "mailto": + ok(event.data.msg == "resource with mailto protocol loaded", "resource with mailto protocol did not load"); + testsToRun["mailto"] = true; + break; + } + checkTestsCompleted(); + } + + function startTest() { + //Set the first set of settings (true, true) and increment the counter. + changePrefs(function() { + // listen for a messages from the mixed content test harness + window.addEventListener("message", receiveMessage, false); + + reloadFrame(); + }); + } + + SimpleTest.waitForExplicitFinish(); + </script> +</head> + +<body onload='startTest()'> + <div id="framediv"></div> + <pre id="log"></pre> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/test_frameNavigation.html b/dom/security/test/mixedcontentblocker/test_frameNavigation.html new file mode 100644 index 000000000..5b3ae50b0 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/test_frameNavigation.html @@ -0,0 +1,127 @@ +<!DOCTYPE HTML> +<html> +<!-- +Tests for Mixed Content Blocker +https://bugzilla.mozilla.org/show_bug.cgi?id=840388 +--> +<head> + <meta charset="utf-8"> + <title>Tests for Bug 840388</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script> + var counter = 0; + var origBlockActive = SpecialPowers.getBoolPref("security.mixed_content.block_active_content"); + + SpecialPowers.setBoolPref("security.mixed_content.block_active_content", true); + var blockActive = SpecialPowers.getBoolPref("security.mixed_content.block_active_content"); + + + var testsToRunInsecure = { + insecurePage_navigate_child: false, + insecurePage_navigate_grandchild: false, + }; + + var testsToRunSecure = { + securePage_navigate_child: false, + blankTarget: false, + }; + + function log(msg) { + document.getElementById("log").textContent += "\n" + msg; + } + + var secureTestsStarted = false; + function checkTestsCompleted() { + for (var prop in testsToRunInsecure) { + // some test hasn't run yet so we're not done + if (!testsToRunInsecure[prop]) + return; + } + // If we are here, all the insecure tests have run. + // If we haven't changed the iframe to run the secure tests, change it now. + if (!secureTestsStarted) { + document.getElementById('testing_frame').src = "https://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_secure.html"; + secureTestsStarted = true; + } + for (var prop in testsToRunSecure) { + // some test hasn't run yet so we're not done + if (!testsToRunSecure[prop]) + return; + } + //if the secure and insecure testsToRun are all completed, change the block mixed active content pref and run the tests again. + if(counter < 1) { + for (var prop in testsToRunSecure) { + testsToRunSecure[prop] = false; + } + for (var prop in testsToRunInsecure) { + testsToRunInsecure[prop] = false; + } + //call to change the preferences + counter++; + SpecialPowers.setBoolPref("security.mixed_content.block_active_content", false); + blockActive = SpecialPowers.getBoolPref("security.mixed_content.block_active_content"); + log("blockActive set to "+blockActive+"."); + secureTestsStarted = false; + document.getElementById('framediv').innerHTML = '<iframe src="http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation.html" id="testing_frame"></iframe>'; + } + else { + //set the prefs back to what they were set to originally + SpecialPowers.setBoolPref("security.mixed_content.block_active_content", origBlockActive); + SimpleTest.finish(); + } + } + + var firstTestDebugMessage = true; + + // listen for a messages from the mixed content test harness + window.addEventListener("message", receiveMessage, false); + function receiveMessage(event) { + if(firstTestDebugMessage) { + log("blockActive set to "+blockActive); + firstTestDebugMessage = false; + } + + log("test: "+event.data.test+", msg: "+event.data.msg + "."); + // test that the load type matches the pref for this type of content + // (i.e. active vs. display) + + switch(event.data.test) { + + case "insecurePage_navigate_child": + is(event.data.msg, "navigated to insecure iframe on insecure page", "navigating to insecure iframe blocked on insecure page"); + testsToRunInsecure["insecurePage_navigate_child"] = true; + break; + + case "insecurePage_navigate_grandchild": + is(event.data.msg, "navigated to insecure grandchild iframe on insecure page", "navigating to insecure grandchild iframe blocked on insecure page"); + testsToRunInsecure["insecurePage_navigate_grandchild"] = true; + break; + + case "securePage_navigate_child": + ok(blockActive == (event.data.msg == "navigating to insecure iframe blocked on secure page"), "navigated to insecure iframe on secure page"); + testsToRunSecure["securePage_navigate_child"] = true; + break; + + case "blankTarget": + is(event.data.msg, "opened an http link with target=_blank from a secure page", "couldn't open an http link in a new window from a secure page"); + testsToRunSecure["blankTarget"] = true; + break; + + } + checkTestsCompleted(); + } + + SimpleTest.waitForExplicitFinish(); + </script> +</head> + +<body> + <div id="framediv"> + <iframe src="http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation.html" id="testing_frame"></iframe> + </div> + + <pre id="log"></pre> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/test_main.html b/dom/security/test/mixedcontentblocker/test_main.html new file mode 100644 index 000000000..d2bc9dc7e --- /dev/null +++ b/dom/security/test/mixedcontentblocker/test_main.html @@ -0,0 +1,187 @@ +<!DOCTYPE HTML> +<html> +<!-- +Tests for Mixed Content Blocker +https://bugzilla.mozilla.org/show_bug.cgi?id=62178 +--> +<head> + <meta charset="utf-8"> + <title>Tests for Bug 62178</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script> + SpecialPowers.setTestPluginEnabledState(SpecialPowers.Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in"); + + var counter = 0; + var settings = [ [true, true], [true, false], [false, true], [false, false] ]; + + var blockActive; + var blockDisplay; + + //Cycle through 4 different preference settings. + function changePrefs(otherPrefs, callback) { + let basePrefs = [["security.mixed_content.block_display_content", settings[counter][0]], + ["security.mixed_content.block_active_content", settings[counter][1]]]; + let newPrefs = basePrefs.concat(otherPrefs); + + SpecialPowers.pushPrefEnv({"set": newPrefs}, function () { + blockDisplay = SpecialPowers.getBoolPref("security.mixed_content.block_display_content"); + blockActive = SpecialPowers.getBoolPref("security.mixed_content.block_active_content"); + counter++; + callback(); + }); + } + + var testsToRun = { + iframe: false, + image: false, + imageSrcset: false, + imageSrcsetFallback: false, + imagePicture: false, + imageJoinPicture: false, + imageLeavePicture: false, + script: false, + stylesheet: false, + object: false, + media: false, + xhr: false, + }; + + function log(msg) { + document.getElementById("log").textContent += "\n" + msg; + } + + function reloadFrame() { + document.getElementById('framediv').innerHTML = '<iframe id="testHarness" src="https://example.com/tests/dom/security/test/mixedcontentblocker/file_main.html"></iframe>'; + } + + function checkTestsCompleted() { + for (var prop in testsToRun) { + // some test hasn't run yet so we're not done + if (!testsToRun[prop]) + return; + } + //if the testsToRun are all completed, chnage the pref and run the tests again until we have cycled through all the prefs. + if(counter < 4) { + for (var prop in testsToRun) { + testsToRun[prop] = false; + } + //call to change the preferences + changePrefs([], function() { + log("\nblockDisplay set to "+blockDisplay+", blockActive set to "+blockActive+"."); + reloadFrame(); + }); + } + else { + SimpleTest.finish(); + } + } + + var firstTest = true; + + function receiveMessage(event) { + if(firstTest) { + log("blockActive set to "+blockActive+", blockDisplay set to "+blockDisplay+"."); + firstTest = false; + } + + log("test: "+event.data.test+", msg: "+event.data.msg + " logging message."); + // test that the load type matches the pref for this type of content + // (i.e. active vs. display) + + switch(event.data.test) { + + /* Mixed Script tests */ + case "iframe": + ok(blockActive == (event.data.msg == "insecure iframe blocked"), "iframe did not follow block_active_content pref"); + testsToRun["iframe"] = true; + break; + + case "object": + ok(blockActive == (event.data.msg == "insecure object blocked"), "object did not follow block_active_content pref"); + testsToRun["object"] = true; + break; + + case "script": + ok(blockActive == (event.data.msg == "insecure script blocked"), "script did not follow block_active_content pref"); + testsToRun["script"] = true; + break; + + case "stylesheet": + ok(blockActive == (event.data.msg == "insecure stylesheet blocked"), "stylesheet did not follow block_active_content pref"); + testsToRun["stylesheet"] = true; + break; + + case "xhr": + ok(blockActive == (event.data.msg == "insecure xhr blocked"), "xhr did not follow block_active_content pref"); + testsToRun["xhr"] = true; + break; + + /* Mixed Display tests */ + case "image": + //test that the image load matches the pref for display content + ok(blockDisplay == (event.data.msg == "insecure image blocked"), "image did not follow block_display_content pref"); + testsToRun["image"] = true; + break; + + case "media": + ok(blockDisplay == (event.data.msg == "insecure media blocked"), "media did not follow block_display_content pref"); + testsToRun["media"] = true; + break; + + /* Images using the "imageset" policy, from <img srcset> and <picture>, do not get the mixed display exception */ + case "imageSrcset": + ok(blockActive == (event.data.msg == "insecure image blocked"), "imageSrcset did not follow block_active_content pref"); + testsToRun["imageSrcset"] = true; + break; + + case "imageSrcsetFallback": + ok(blockActive == (event.data.msg == "insecure image blocked"), "imageSrcsetFallback did not follow block_active_content pref"); + testsToRun["imageSrcsetFallback"] = true; + break; + + case "imagePicture": + ok(blockActive == (event.data.msg == "insecure image blocked"), "imagePicture did not follow block_active_content pref"); + testsToRun["imagePicture"] = true; + break; + + case "imageJoinPicture": + ok(blockActive == (event.data.msg == "insecure image blocked"), "imageJoinPicture did not follow block_active_content pref"); + testsToRun["imageJoinPicture"] = true; + break; + + // Should return to mixed display mode + case "imageLeavePicture": + ok(blockDisplay == (event.data.msg == "insecure image blocked"), "imageLeavePicture did not follow block_display_content pref"); + testsToRun["imageLeavePicture"] = true; + break; + + } + checkTestsCompleted(); + } + + function startTest() { + // Set prefs to use mixed-content before HSTS + SpecialPowers.pushPrefEnv({'set': [["security.mixed_content.use_hsts", false], + ["security.mixed_content.send_hsts_priming", false]]}); + //Set the first set of mixed content settings and increment the counter. + changePrefs([], function() { + //listen for a messages from the mixed content test harness + window.addEventListener("message", receiveMessage, false); + + //Kick off test + reloadFrame(); + }); + } + + SimpleTest.waitForExplicitFinish(); + + </script> +</head> + +<body onload='startTest()'> + <div id="framediv"></div> + <pre id="log"></pre> +</body> +</html> diff --git a/dom/security/test/moz.build b/dom/security/test/moz.build new file mode 100644 index 000000000..ddb4e9b89 --- /dev/null +++ b/dom/security/test/moz.build @@ -0,0 +1,31 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPCSHELL_TESTS_MANIFESTS += [ + 'unit/xpcshell.ini', +] + +TEST_DIRS += [ + 'gtest', +] + +MOCHITEST_MANIFESTS += [ + 'cors/mochitest.ini', + 'csp/mochitest.ini', + 'general/mochitest.ini', + 'mixedcontentblocker/mochitest.ini', + 'sri/mochitest.ini', +] + +MOCHITEST_CHROME_MANIFESTS += [ + 'general/chrome.ini', +] + +BROWSER_CHROME_MANIFESTS += [ + 'contentverifier/browser.ini', + 'csp/browser.ini', + 'hsts/browser.ini', +] diff --git a/dom/security/test/sri/file_bug_1271796.css b/dom/security/test/sri/file_bug_1271796.css new file mode 100644 index 000000000..c0928f2cf --- /dev/null +++ b/dom/security/test/sri/file_bug_1271796.css @@ -0,0 +1,2 @@ +/*! Simple test for bug 1271796 */ +p::before { content: "\2014"; } diff --git a/dom/security/test/sri/iframe_csp_directive_style_imports.html b/dom/security/test/sri/iframe_csp_directive_style_imports.html new file mode 100644 index 000000000..0abea7854 --- /dev/null +++ b/dom/security/test/sri/iframe_csp_directive_style_imports.html @@ -0,0 +1,6 @@ +<!-- file should be loaded (text is blue), but subsequent files shouldn't (text is red) --> +<link rel="stylesheet" href="style_importing.css" + integrity="sha384-m5Q2GOhAtLrdiv6rCmxY3GjEFMVInALcdTyDnEddUUiDH2uQvJSX5GSJYQiatpTK" + onload="parent.postMessage('finish', '*');" + onerror="parent.postMessage('finish', '*');"> +<p id="text-for-import-test">blue text</p> diff --git a/dom/security/test/sri/iframe_csp_directive_style_imports.html^headers^ b/dom/security/test/sri/iframe_csp_directive_style_imports.html^headers^ new file mode 100644 index 000000000..0a6ccba79 --- /dev/null +++ b/dom/security/test/sri/iframe_csp_directive_style_imports.html^headers^ @@ -0,0 +1 @@ +content-security-policy: require-sri-for script style diff --git a/dom/security/test/sri/iframe_require-sri-for_main.html b/dom/security/test/sri/iframe_require-sri-for_main.html new file mode 100644 index 000000000..467c699c7 --- /dev/null +++ b/dom/security/test/sri/iframe_require-sri-for_main.html @@ -0,0 +1,47 @@ +<script> + window.hasCORSLoaded = false; // set through script_crossdomain1.js +</script> + +<!-- script tag cors-enabled. should be loaded --> +<script src="http://example.com/tests/dom/security/test/sri/script_crossdomain1.js" + crossorigin="" + integrity="sha512-9Tv2DL1fHvmPQa1RviwKleE/jq72jgxj8XGLyWn3H6Xp/qbtfK/jZINoPFAv2mf0Nn1TxhZYMFULAbzJNGkl4Q==" + onload="parent.postMessage('good_sriLoaded', '*');"></script> + +<!-- script tag cors but not using SRI. should trigger onerror --> +<script src="http://example.com/tests/dom/security/test/sri/script_crossdomain5.js" + onload="parent.postMessage('bad_nonsriLoaded', '*');" + onerror="parent.postMessage('good_nonsriBlocked', '*');"></script> + +<!-- svg:script tag with cors but not using SRI. should trigger onerror --> +<svg xmlns="http://www.w3.org/2000/svg"> + <script xlink:href="http://example.com/tests/dom/security/test/sri/script_crossdomain3.js" + onload="parent.postMessage('bad_svg_nonsriLoaded', '*');" + onerror="parent.postMessage('good_svg_nonsriBlocked', '*');"></script> + ></script> +</svg> + +<!-- stylesheet with cors and integrity. it should just load fine. --> +<link rel="stylesheet" href="style1.css" + integrity="sha256-qs8lnkunWoVldk5d5E+652yth4VTSHohlBKQvvgGwa8=" + onload="parent.postMessage('good_sriLoaded', '*');"> + +<!-- stylesheet not using SRI, should trigger onerror --> +<link rel="stylesheet" href="style3.css" + onload="parent.postMessage('bad_nonsriLoaded', '*');" + onerror="parent.postMessage('good_nonsriBlocked', '*');"> + + +<p id="black-text">black text</p> +<script> + // this worker should not load, + // given that we can not provide integrity metadata through the constructor + w = new Worker("rsf_worker.js"); + w.onerror = function(e) { + if (typeof w == "object") { + parent.postMessage("finish", '*'); + } else { + parent.postMessage("error", "*") + } + } +</script> diff --git a/dom/security/test/sri/iframe_require-sri-for_main.html^headers^ b/dom/security/test/sri/iframe_require-sri-for_main.html^headers^ new file mode 100644 index 000000000..0a6ccba79 --- /dev/null +++ b/dom/security/test/sri/iframe_require-sri-for_main.html^headers^ @@ -0,0 +1 @@ +content-security-policy: require-sri-for script style diff --git a/dom/security/test/sri/iframe_require-sri-for_no_csp.html b/dom/security/test/sri/iframe_require-sri-for_no_csp.html new file mode 100644 index 000000000..435b32ea3 --- /dev/null +++ b/dom/security/test/sri/iframe_require-sri-for_no_csp.html @@ -0,0 +1,5 @@ +<script> + w = new Worker("rsf_csp_worker.js"); + // use the handler function in the parent frame (test_require-sri-for_csp_directive.html) + w.onmessage = parent.handler; +</script> 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 000000000..a752c7e79 --- /dev/null +++ b/dom/security/test/sri/iframe_script_crossdomain.html @@ -0,0 +1,135 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> + +<script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + window.hasCORSLoaded = false; + window.hasNonCORSLoaded = false; + + function good_nonsriLoaded() { + ok(true, "Non-eligible non-SRI resource was loaded correctly."); + } + function bad_nonsriBlocked() { + ok(false, "Non-eligible non-SRI resources should be loaded!"); + } + + function good_nonCORSInvalidBlocked() { + ok(true, "A non-CORS resource with invalid metadata was correctly blocked."); + } + function bad_nonCORSInvalidLoaded() { + ok(false, "Non-CORS resources with invalid metadata should be blocked!"); + } + + window.onerrorCalled = false; + window.onloadCalled = false; + + function bad_onloadCalled() { + window.onloadCalled = true; + } + + function good_onerrorCalled() { + window.onerrorCalled = true; + } + + function good_incorrect301Blocked() { + ok(true, "A non-CORS load with incorrect hash redirected to a different origin was blocked correctly."); + } + function bad_incorrect301Loaded() { + ok(false, "Non-CORS loads with incorrect hashes redirecting to a different origin should be blocked!"); + } + + function good_correct301Blocked() { + ok(true, "A non-CORS load with correct hash redirected to a different origin was blocked correctly."); + } + function bad_correct301Loaded() { + ok(false, "Non-CORS loads with correct hashes redirecting to a different origin should be blocked!"); + } + + function good_correctDataLoaded() { + ok(true, "Since data: URLs are same-origin, they should be loaded."); + } + function bad_correctDataBlocked() { + todo(false, "We should not block scripts in data: URIs!"); + } + function good_correctDataCORSLoaded() { + ok(true, "A data: URL with a CORS load was loaded correctly."); + } + function bad_correctDataCORSBlocked() { + ok(false, "We should not BLOCK scripts!"); + } + + window.onload = function() { + SimpleTest.finish() + } +</script> + +<!-- cors-enabled. should be loaded --> +<script src="http://example.com/tests/dom/security/test/sri/script_crossdomain1.js" + crossorigin="" + integrity="sha512-9Tv2DL1fHvmPQa1RviwKleE/jq72jgxj8XGLyWn3H6Xp/qbtfK/jZINoPFAv2mf0Nn1TxhZYMFULAbzJNGkl4Q=="></script> + +<!-- not cors-enabled. should be blocked --> +<script src="http://example.com/tests/dom/security/test/sri/script_crossdomain2.js" + crossorigin="anonymous" + integrity="sha256-ntgU2U1xv7HfK1XWMTSWz6vJkyVtGzMrIAxQkux1I94=" + onload="bad_onloadCalled()" + onerror="good_onerrorCalled()"></script> + +<!-- non-cors but not actually using SRI. should trigger onload --> +<script src="http://example.com/tests/dom/security/test/sri/script_crossdomain3.js" + integrity=" " + onload="good_nonsriLoaded()" + onerror="bad_nonsriBlocked()"></script> + +<!-- non-cors with invalid metadata --> +<script src="http://example.com/tests/dom/security/test/sri/script_crossdomain4.js" + integrity="sha256-bogus" + onload="bad_nonCORSInvalidLoaded()" + onerror="good_nonCORSInvalidBlocked()"></script> + +<!-- non-cors that's same-origin initially but redirected to another origin --> +<script src="script_301.js" + integrity="sha384-invalid" + onerror="good_incorrect301Blocked()" + onload="bad_incorrect301Loaded()"></script> + +<!-- non-cors that's same-origin initially but redirected to another origin --> +<script src="script_301.js" + integrity="sha384-1NpiDI6decClMaTWSCAfUjTdx1BiOffsCPgH4lW5hCLwmHk0VyV/g6B9Sw2kD2K3" + onerror="good_correct301Blocked()" + onload="bad_correct301Loaded()"></script> + +<!-- data: URLs are same-origin --> +<script src="data:,console.log('data:valid');" + integrity="sha256-W5I4VIN+mCwOfR9kDbvWoY1UOVRXIh4mKRN0Nz0ookg=" + onerror="bad_correctDataBlocked()" + onload="good_correctDataLoaded()"></script> + +<!-- not cors-enabled with data: URLs. should trigger onload --> +<script src="data:,console.log('data:valid');" + crossorigin="anonymous" + integrity="sha256-W5I4VIN+mCwOfR9kDbvWoY1UOVRXIh4mKRN0Nz0ookg=" + onerror="bad_correctDataCORSBlocked()" + onload="good_correctDataCORSLoaded()"></script> + +<script> + ok(window.hasCORSLoaded, "CORS-enabled resource with a correct hash"); + ok(!window.hasNonCORSLoaded, "Correct hash, but non-CORS, should be blocked"); + ok(!window.onloadCalled, "Failed loads should not call onload when they're cross-domain"); + ok(window.onerrorCalled, "Failed loads should call onerror when they're cross-domain"); +</script> +</body> +</html> 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 000000000..b891db547 --- /dev/null +++ b/dom/security/test/sri/iframe_script_sameorigin.html @@ -0,0 +1,249 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + window.onload = function() { + SimpleTest.finish(); + } + </script> + <script> + function good_correctHashLoaded() { + ok(true, "A script was correctly loaded when integrity matched") + } + function bad_correctHashBlocked() { + ok(false, "We should load scripts with hashes that match!"); + } + + function good_correctHashArrayLoaded() { + ok(true, "A script was correctly loaded when one of the hashes in the integrity attribute matched") + } + function bad_correctHashArrayBlocked() { + ok(false, "We should load scripts with at least one hash that match!"); + } + + function good_emptyIntegrityLoaded() { + ok(true, "A script was correctly loaded when the integrity attribute was empty") + } + function bad_emptyIntegrityBlocked() { + ok(false, "We should load scripts with empty integrity attributes!"); + } + + function good_whitespaceIntegrityLoaded() { + ok(true, "A script was correctly loaded when the integrity attribute only contained whitespace") + } + function bad_whitespaceIntegrityBlocked() { + ok(false, "We should load scripts with integrity attributes containing only whitespace!"); + } + + function good_incorrectHashBlocked() { + ok(true, "A script was correctly blocked, because the hash digest was wrong"); + } + function bad_incorrectHashLoaded() { + ok(false, "We should not load scripts with hashes that do not match the content!"); + } + + function good_incorrectHashArrayBlocked() { + ok(true, "A script was correctly blocked, because all the hashes were wrong"); + } + function bad_incorrectHashArrayLoaded() { + ok(false, "We should not load scripts when none of the hashes match the content!"); + } + + function good_incorrectHashLengthBlocked() { + ok(true, "A script was correctly blocked, because the hash length was wrong"); + } + function bad_incorrectHashLengthLoaded() { + ok(false, "We should not load scripts with hashes that don't have the right length!"); + } + + function bad_incorrectHashFunctionBlocked() { + ok(false, "We should load scripts with invalid/unsupported hash functions!"); + } + function good_incorrectHashFunctionLoaded() { + ok(true, "A script was correctly loaded, despite the hash function being invalid/unsupported."); + } + + function bad_missingHashFunctionBlocked() { + ok(false, "We should load scripts with missing hash functions!"); + } + function good_missingHashFunctionLoaded() { + ok(true, "A script was correctly loaded, despite a missing hash function."); + } + + function bad_missingHashValueBlocked() { + ok(false, "We should load scripts with missing hash digests!"); + } + function good_missingHashValueLoaded() { + ok(true, "A script was correctly loaded, despite the missing hash digest."); + } + + function good_401Blocked() { + ok(true, "A script was not loaded because of 401 response."); + } + function bad_401Loaded() { + ok(false, "We should nt load scripts with a 401 response!"); + } + + function good_valid302Loaded() { + ok(true, "A script was loaded successfully despite a 302 response."); + } + function bad_valid302Blocked() { + ok(false, "We should load scripts with a 302 response and the right hash!"); + } + + function good_invalid302Blocked() { + ok(true, "A script was blocked successfully after a 302 response."); + } + function bad_invalid302Loaded() { + ok(false, "We should not load scripts with a 302 response and the wrong hash!"); + } + + function good_validBlobLoaded() { + ok(true, "A script was loaded successfully from a blob: URL."); + } + function bad_validBlobBlocked() { + ok(false, "We should load scripts using blob: URLs with the right hash!"); + } + + function good_invalidBlobBlocked() { + ok(true, "A script was blocked successfully from a blob: URL."); + } + function bad_invalidBlobLoaded() { + ok(false, "We should not load scripts using blob: URLs with the wrong hash!"); + } +</script> +</head> +<body> + <!-- valid hash. should trigger onload --> + <!-- the hash value comes from running this command: + cat script.js | openssl dgst -sha256 -binary | openssl enc -base64 -A + --> + <script src="script.js" + integrity="sha256-RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA=" + onerror="bad_correctHashBlocked()" + onload="good_correctHashLoaded()"></script> + + <!-- valid sha512 hash. should trigger onload --> + <script src="script.js" + integrity="sha512-mzSqH+vC6qrXX46JX2WEZ0FtY/lGj/5+5yYCBlk0jfYHLm0vP6XgsURbq83mwMApsnwbDLXdgjp5J8E93GT6Mw==?ignore=this" + onerror="bad_correctHashBlocked()" + onload="good_correctHashLoaded()"></script> + + <!-- one valid sha256 hash. should trigger onload --> + <script src="script.js" + integrity="sha256-rkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA= sha256-RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA= sha256-rkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA=" + onerror="bad_correctHashArrayBlocked()" + onload="good_correctHashArrayLoaded()"></script> + + <!-- empty integrity. should trigger onload --> + <script src="script.js" + integrity="" + onerror="bad_emptyIntegrityBlocked()" + onload="good_emptyIntegrityLoaded()"></script> + + <!-- whitespace integrity. should trigger onload --> + <script src="script.js" + integrity=" + +" + onerror="bad_whitespaceIntegrityBlocked()" + onload="good_whitespaceIntegrityLoaded()"></script> + + <!-- invalid sha256 hash but valid sha384 hash. should trigger onload --> + <script src="script.js" + integrity="sha256-bogus sha384-zDCkvKOHXk8mM6Nk07oOGXGME17PA4+ydFw+hq0r9kgF6ZDYFWK3fLGPEy7FoOAo?" + onerror="bad_correctHashBlocked()" + onload="good_correctHashLoaded()"></script> + + <!-- valid sha256 and invalid sha384. should trigger onerror --> + <script src="script.js" + integrity="sha256-RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA= sha384-RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA=" + onerror="good_incorrectHashLengthBlocked()" + onload="bad_incorrectHashLengthLoaded()"></script> + + <!-- invalid hash. should trigger onerror --> + <script src="script.js" + integrity="sha256-rkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA=" + onerror="good_incorrectHashBlocked()" + onload="bad_incorrectHashLoaded()"></script> + + <!-- invalid hashes. should trigger onerror --> + <script src="script.js" + integrity="sha256-rkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA= sha256-ZkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA= sha256-zkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA=" + onerror="good_incorrectHashBlocked()" + onload="bad_incorrectHashLoaded()"></script> + + <!-- invalid hash function. should trigger onload --> + <script src="script.js" + integrity="rot13-RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA=" + onerror="bad_incorrectHashFunctionBlocked()" + onload="good_incorrectHashFunctionLoaded()"></script> + + <!-- missing hash function. should trigger onload --> + <script src="script.js" + integrity="RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA=" + onerror="bad_missingHashFunctionBlocked()" + onload="good_missingHashFunctionLoaded()"></script> + + <!-- missing hash value. should trigger onload --> + <script src="script.js" + integrity="sha512-" + onerror="bad_missingHashValueBlocked()" + onload="good_missingHashValueLoaded()"></script> + + <!-- 401 response. should trigger onerror --> + <script src="script_401.js" + integrity="sha256-RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA=" + onerror="good_401Blocked()" + onload="bad_401Loaded()"></script> + + <!-- valid sha256 after a redirection. should trigger onload --> + <script src="script_302.js" + integrity="sha256-RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA=" + onerror="bad_valid302Blocked()" + onload="good_valid302Loaded()"></script> + + <!-- invalid sha256 after a redirection. should trigger onerror --> + <script src="script_302.js" + integrity="sha256-JSi74NSN8WQNr9syBGmNg2APJp9PnHUO5ioZo5hmIiQ=" + onerror="good_invalid302Blocked()" + onload="bad_invalid302Loaded()"></script> + + <!-- valid sha256 for a blob: URL --> + <script> + var blob = new Blob(["console.log('blob:valid');"], + {type:"application/javascript"}); + var script = document.createElement('script'); + script.setAttribute('src', URL.createObjectURL(blob)); + script.setAttribute('integrity', 'sha256-AwLdXiGfCqOxOXDPUim73G8NVEL34jT0IcQR/tqv/GQ='); + script.onerror = bad_validBlobBlocked; + script.onload = good_validBlobLoaded; + var head = document.getElementsByTagName('head').item(0); + head.appendChild(script); + </script> + + <!-- invalid sha256 for a blob: URL --> + <script> + var blob = new Blob(["console.log('blob:invalid');"], + {type:"application/javascript"}); + var script = document.createElement('script'); + script.setAttribute('src', URL.createObjectURL(blob)); + script.setAttribute('integrity', 'sha256-AwLdXiGfCqOxOXDPUim73G8NVEL34jT0IcQR/tqv/GQ='); + script.onerror = good_invalidBlobBlocked; + script.onload = bad_invalidBlobLoaded; + var head = document.getElementsByTagName('head').item(0); + head.appendChild(script); + </script> + +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</html> 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 000000000..9fb10293a --- /dev/null +++ b/dom/security/test/sri/iframe_sri_disabled.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + window.onload = function() { + SimpleTest.finish(); + } + </script> + <script> + function good_correctHashLoaded() { + ok(true, "A script was correctly loaded when integrity matched") + } + function bad_correctHashBlocked() { + ok(false, "We should load scripts with hashes that match!"); + } + + function good_incorrectHashLoaded() { + ok(true, "A script was correctly loaded despite the incorrect hash because SRI is disabled."); + } + function bad_incorrectHashBlocked() { + ok(false, "We should load scripts with hashes that do not match the content when SRI is disabled!"); + } + + function good_correctStyleHashLoaded() { + ok(true, "A stylesheet was correctly loaded when integrity matched") + } + function bad_correctStyleHashBlocked() { + ok(false, "We should load stylesheets with hashes that match!"); + } + + function good_incorrectStyleHashLoaded() { + ok(true, "A stylesheet was correctly loaded despite the incorrect hash because SRI is disabled."); + } + function bad_incorrectStyleHashBlocked() { + ok(false, "We should load stylesheets with hashes that do not match the content when SRI is disabled!"); + } + </script> + + <!-- valid sha256 hash. should trigger onload --> + <link rel="stylesheet" href="style1.css?disabled" + integrity="sha256-qs8lnkunWoVldk5d5E+652yth4VTSHohlBKQvvgGwa8=" + onerror="bad_correctStyleHashBlocked()" + onload="good_correctStyleHashLoaded()"> + + <!-- invalid sha256 hash. should trigger onerror --> + <link rel="stylesheet" href="style2.css?disabled" + integrity="sha256-bogus" + onerror="bad_incorrectStyleHashBlocked()" + onload="good_incorrectStyleHashLoaded()"> +</head> +<body> + <!-- valid hash. should trigger onload --> + <script src="script.js" + integrity="sha256-RkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA=" + onerror="bad_correctHashBlocked()" + onload="good_correctHashLoaded()"></script> + + <!-- invalid hash. should trigger onerror --> + <script src="script.js" + integrity="sha256-rkrQYrxD/HCx+ImVLb51nvxJ6ZHfwuEm7bHppTun9oA=" + onerror="bad_incorrectHashBlocked()" + onload="good_incorrectHashLoaded()"></script> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</html> 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 000000000..22c8593b5 --- /dev/null +++ b/dom/security/test/sri/iframe_style_crossdomain.html @@ -0,0 +1,117 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + function check_styles() { + var redText = document.getElementById('red-text'); + var greenText = document.getElementById('green-text'); + var blueText = document.getElementById('blue-text'); + var redTextColor = window.getComputedStyle(redText, null).getPropertyValue('color'); + var greenTextColor = window.getComputedStyle(greenText, null).getPropertyValue('color'); + var blueTextColor = window.getComputedStyle(blueText, null).getPropertyValue('color'); + ok(redTextColor == 'rgb(255, 0, 0)', "The first part should be red."); + ok(greenTextColor == 'rgb(0, 255, 0)', "The second part should be green."); + ok(blueTextColor == 'rgb(0, 0, 255)', "The third part should be blue."); + } + + SimpleTest.waitForExplicitFinish(); + window.onload = function() { + check_styles(); + SimpleTest.finish(); + } + </script> + <script> + function good_correctHashCORSLoaded() { + ok(true, "A CORS cross-domain stylesheet with correct hash was correctly loaded."); + } + function bad_correctHashCORSBlocked() { + ok(false, "We should load CORS cross-domain stylesheets with hashes that match!"); + } + function good_correctHashBlocked() { + ok(true, "A non-CORS cross-domain stylesheet with correct hash was correctly blocked."); + } + function bad_correctHashLoaded() { + ok(false, "We should block non-CORS cross-domain stylesheets with hashes that match!"); + } + + function good_incorrectHashBlocked() { + ok(true, "A non-CORS cross-domain stylesheet with incorrect hash was correctly blocked."); + } + function bad_incorrectHashLoaded() { + ok(false, "We should load non-CORS cross-domain stylesheets with incorrect hashes!"); + } + + function bad_correctDataBlocked() { + ok(false, "We should not block non-CORS cross-domain stylesheets in data: URI!"); + } + function good_correctDataLoaded() { + ok(true, "A non-CORS cross-domain stylesheet with data: URI was correctly loaded."); + } + function bad_correctDataCORSBlocked() { + ok(false, "We should not block CORS stylesheets in data: URI!"); + } + function good_correctDataCORSLoaded() { + ok(true, "A CORS stylesheet with data: URI was correctly loaded."); + } + + function good_correctHashOpaqueBlocked() { + ok(true, "A non-CORS(Opaque) cross-domain stylesheet with correct hash was correctly blocked."); + } + function bad_correctHashOpaqueLoaded() { + ok(false, "We should not load non-CORS(Opaque) cross-domain stylesheets with correct hashes!"); + } + </script> + + <!-- valid CORS sha256 hash --> + <link rel="stylesheet" href="http://example.com/tests/dom/security/test/sri/style1.css" + crossorigin="anonymous" + integrity="sha256-qs8lnkunWoVldk5d5E+652yth4VTSHohlBKQvvgGwa8=" + onerror="bad_correctHashCORSBlocked()" + onload="good_correctHashCORSLoaded()"> + + <!-- valid non-CORS sha256 hash --> + <link rel="stylesheet" href="style_301.css" + integrity="sha256-qs8lnkunWoVldk5d5E+652yth4VTSHohlBKQvvgGwa8=" + onerror="good_correctHashBlocked()" + onload="bad_correctHashLoaded()"> + + <!-- invalid non-CORS sha256 hash --> + <link rel="stylesheet" href="style_301.css?again" + integrity="sha256-bogus" + onerror="good_incorrectHashBlocked()" + onload="bad_incorrectHashLoaded()"> + + <!-- valid non-CORS sha256 hash in a data: URL --> + <link rel="stylesheet" href="data:text/css,.green-text{color:rgb(0, 255, 0)}" + integrity="sha256-EhVtGGyovvffvYdhyqJxUJ/ekam7zlxxo46iM13cwP0=" + onerror="bad_correctDataBlocked()" + onload="good_correctDataLoaded()"> + + <!-- valid CORS sha256 hash in a data: URL --> + <link rel="stylesheet" href="data:text/css,.blue-text{color:rgb(0, 0, 255)}" + crossorigin="anonymous" + integrity="sha256-m0Fs2hNSyPOn1030Dp+c8pJFHNmwpeTbB+8J/DcqLss=" + onerror="bad_correctDataCORSBlocked()" + onload="good_correctDataCORSLoaded()"> + + <!-- valid non-CORS sha256 hash --> + <link rel="stylesheet" href="http://example.com/tests/dom/security/test/sri/style1.css" + integrity="sha256-qs8lnkunWoVldk5d5E+652yth4VTSHohlBKQvvgGwa8=" + onerror="good_correctHashOpaqueBlocked()" + onload="bad_correctHashOpaqueLoaded()"> +</head> +<body> +<p><span id="red-text">This should be red</span> but + <span id="green-text" class="green-text">this should be green</span> and + <span id="blue-text" class="blue-text">this should be blue</span></p> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</html> 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 000000000..d994e7c2b --- /dev/null +++ b/dom/security/test/sri/iframe_style_sameorigin.html @@ -0,0 +1,164 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + function check_styles() { + var redText = document.getElementById('red-text'); + var blueText = document.getElementById('blue-text-element'); + var blackText1 = document.getElementById('black-text'); + var blackText2 = document.getElementById('black-text-2'); + var redTextColor = window.getComputedStyle(redText, null).getPropertyValue('color'); + var blueTextColor = window.getComputedStyle(blueText, null).getPropertyValue('color'); + var blackTextColor1 = window.getComputedStyle(blackText1, null).getPropertyValue('color'); + var blackTextColor2 = window.getComputedStyle(blackText2, null).getPropertyValue('color'); + ok(redTextColor == 'rgb(255, 0, 0)', "The first part should be red."); + ok(blueTextColor == 'rgb(0, 0, 255)', "The second part should be blue."); + ok(blackTextColor1 == 'rgb(0, 0, 0)', "The second last part should still be black."); + ok(blackTextColor2 == 'rgb(0, 0, 0)', "The last part should still be black."); + } + + SimpleTest.waitForExplicitFinish(); + window.onload = function() { + check_styles(); + SimpleTest.finish(); + } + </script> + <script> + function good_correctHashLoaded() { + ok(true, "A stylesheet was correctly loaded when integrity matched"); + } + function bad_correctHashBlocked() { + ok(false, "We should load stylesheets with hashes that match!"); + } + + function good_emptyIntegrityLoaded() { + ok(true, "A stylesheet was correctly loaded when the integrity attribute was empty"); + } + function bad_emptyIntegrityBlocked() { + ok(false, "We should load stylesheets with empty integrity attributes!"); + } + + function good_incorrectHashBlocked() { + ok(true, "A stylesheet was correctly blocked, because the hash digest was wrong"); + } + function bad_incorrectHashLoaded() { + ok(false, "We should not load stylesheets with hashes that do not match the content!"); + } + + function good_validBlobLoaded() { + ok(true, "A stylesheet was loaded successfully from a blob: URL with the right hash."); + } + function bad_validBlobBlocked() { + ok(false, "We should load stylesheets using blob: URLs with the right hash!"); + } + function good_invalidBlobBlocked() { + ok(true, "A stylesheet was blocked successfully from a blob: URL with an invalid hash."); + } + function bad_invalidBlobLoaded() { + ok(false, "We should not load stylesheets using blob: URLs when they have the wrong hash!"); + } + + function good_correctUTF8HashLoaded() { + ok(true, "A UTF8 stylesheet was correctly loaded when integrity matched"); + } + function bad_correctUTF8HashBlocked() { + ok(false, "We should load UTF8 stylesheets with hashes that match!"); + } + function good_correctUTF8BOMHashLoaded() { + ok(true, "A UTF8 stylesheet (with BOM) was correctly loaded when integrity matched"); + } + function bad_correctUTF8BOMHashBlocked() { + ok(false, "We should load UTF8 (with BOM) stylesheets with hashes that match!"); + } + function good_correctUTF8ishHashLoaded() { + ok(true, "A UTF8ish stylesheet was correctly loaded when integrity matched"); + } + function bad_correctUTF8ishHashBlocked() { + ok(false, "We should load UTF8ish stylesheets with hashes that match!"); + } + </script> + + <!-- valid sha256 hash. should trigger onload --> + <link rel="stylesheet" href="style1.css" + integrity="sha256-qs8lnkunWoVldk5d5E+652yth4VTSHohlBKQvvgGwa8=" + onerror="bad_correctHashBlocked()" + onload="good_correctHashLoaded()"> + + <!-- empty metadata. should trigger onload --> + <link rel="stylesheet" href="style2.css" + integrity="" + onerror="bad_emptyIntegrityBlocked()" + onload="good_emptyIntegrityLoaded()"> + + <!-- invalid sha256 hash. should trigger onerror --> + <link rel="stylesheet" href="style3.css" + integrity="sha256-bogus" + onerror="good_incorrectHashBlocked()" + onload="bad_incorrectHashLoaded()"> + + <!-- valid sha384 hash of a utf8 file. should trigger onload --> + <link rel="stylesheet" href="style4.css" + integrity="sha384-13rt+j7xMDLhohLukb7AZx8lDGS3hkahp0IoeuyvxSNVPyc1QQmTDcwXGhQZjoMH" + onerror="bad_correctUTF8HashBlocked()" + onload="good_correctUTF8HashLoaded()"> + + <!-- valid sha384 hash of a utf8 file with a BOM. should trigger onload --> + <link rel="stylesheet" href="style5.css" + integrity="sha384-udAqVKPIHf/OD1isAYKrgzsog/3Q6lSEL2nKhtLSTmHryiae0+y6x1akeTzEF446" + onerror="bad_correctUTF8BOMHashBlocked()" + onload="good_correctUTF8BOMHashLoaded()"> + + <!-- valid sha384 hash of a utf8 file with the wrong charset. should trigger onload --> + <link rel="stylesheet" href="style6.css" + integrity="sha384-Xli4ROFoVGCiRgXyl7y8jv5Vm2yuqj+8tkNL3cUI7AHaCocna75JLs5xID437W6C" + onerror="bad_correctUTF8ishHashBlocked()" + onload="good_correctUTF8ishHashLoaded()"> +</head> +<body> + +<!-- valid sha256 for a blob: URL --> +<script> + var blob = new Blob(['.blue-text{color:blue}'], + {type: 'text/css'}); + var link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = window.URL.createObjectURL(blob); + link.setAttribute('integrity', 'sha256-/F+EMVnTWYJOAzN5n7/21idiydu6nRi33LZOISZtwOM='); + link.onerror = bad_validBlobBlocked; + link.onload = good_validBlobLoaded; + document.body.appendChild(link); +</script> + +<!-- invalid sha256 for a blob: URL --> +<script> + var blob = new Blob(['.black-text{color:blue}'], + {type: 'text/css'}); + var link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = window.URL.createObjectURL(blob); + link.setAttribute('integrity', 'sha256-/F+EMVnTWYJOAzN5n7/21idiydu6nRi33LZOISZtwOM='); + link.onerror = good_invalidBlobBlocked; + link.onload = bad_invalidBlobLoaded; + document.body.appendChild(link); +</script> + +<p><span id="red-text">This should be red </span>, + <span id="purple-text">this should be purple</span>, + <span id="brown-text">this should be brown</span>, + <span id="orange-text">this should be orange</span>, and + <span class="blue-text" id="blue-text-element">this should be blue.</span> + However, <span id="black-text">this should stay black</span> and + <span class="black-text" id="black-text-2">this should also stay black.</span> +</p> + +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/security/test/sri/mochitest.ini b/dom/security/test/sri/mochitest.ini new file mode 100644 index 000000000..e6e2e2fc3 --- /dev/null +++ b/dom/security/test/sri/mochitest.ini @@ -0,0 +1,57 @@ +[DEFAULT] +support-files = + file_bug_1271796.css + iframe_csp_directive_style_imports.html + iframe_csp_directive_style_imports.html^headers^ + iframe_require-sri-for_main.html + iframe_require-sri-for_main.html^headers^ + iframe_require-sri-for_no_csp.html + iframe_script_crossdomain.html + iframe_script_sameorigin.html + iframe_sri_disabled.html + iframe_style_crossdomain.html + iframe_style_sameorigin.html + rsf_csp_worker.js + rsf_csp_worker.js^headers^ + rsf_imported.js + rsf_worker.js + 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 + style4.css + style4.css^headers^ + style5.css + style6.css + style6.css^headers^ + style_301.css + style_301.css^headers^ + style_importing.css + style_imported.css + +[test_script_sameorigin.html] +[test_script_crossdomain.html] +[test_sri_disabled.html] +[test_style_crossdomain.html] +[test_style_sameorigin.html] +[test_require-sri-for_csp_directive.html] +[test_require-sri-for_csp_directive_disabled.html] +[test_bug_1271796.html] +[test_csp_directive_style_imports.html] diff --git a/dom/security/test/sri/rsf_csp_worker.js b/dom/security/test/sri/rsf_csp_worker.js new file mode 100644 index 000000000..553fdf92b --- /dev/null +++ b/dom/security/test/sri/rsf_csp_worker.js @@ -0,0 +1,9 @@ +postMessage("good_worker_could_load"); +try { + importScripts('rsf_imported.js'); +} catch(e) { + postMessage("good_worker_after_importscripts"); +} +finally { + postMessage("finish"); +} diff --git a/dom/security/test/sri/rsf_csp_worker.js^headers^ b/dom/security/test/sri/rsf_csp_worker.js^headers^ new file mode 100644 index 000000000..0a6ccba79 --- /dev/null +++ b/dom/security/test/sri/rsf_csp_worker.js^headers^ @@ -0,0 +1 @@ +content-security-policy: require-sri-for script style diff --git a/dom/security/test/sri/rsf_imported.js b/dom/security/test/sri/rsf_imported.js new file mode 100644 index 000000000..33f54b7a0 --- /dev/null +++ b/dom/security/test/sri/rsf_imported.js @@ -0,0 +1 @@ +postMessage('bad_worker_could_load_via_importScripts'); diff --git a/dom/security/test/sri/rsf_spawn_CSPd_worker.js b/dom/security/test/sri/rsf_spawn_CSPd_worker.js new file mode 100644 index 000000000..652c77100 --- /dev/null +++ b/dom/security/test/sri/rsf_spawn_CSPd_worker.js @@ -0,0 +1,3 @@ +w = new Worker("rsf_csp_worker.js"); +// use the handler function in test_require-sri-for_csp_directive.html +w.onmessage = parent.handler; diff --git a/dom/security/test/sri/rsf_worker.js b/dom/security/test/sri/rsf_worker.js new file mode 100644 index 000000000..553d3deee --- /dev/null +++ b/dom/security/test/sri/rsf_worker.js @@ -0,0 +1,2 @@ +parent.postMessage('bad_worker_could_load', '*'); +importScripts('rsf_imported.js'); diff --git a/dom/security/test/sri/script.js b/dom/security/test/sri/script.js new file mode 100644 index 000000000..8fd8f96b2 --- /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 000000000..b77232d81 --- /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 000000000..9a95de77c --- /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 000000000..efbfb7334 --- /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 000000000..9a95de77c --- /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 000000000..05a545a6a --- /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 000000000..8fd8f96b2 --- /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 000000000..889fbe081 --- /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 000000000..1f17a6db2 --- /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 000000000..3a6a85d89 --- /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 000000000..4b0208ab3 --- /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 000000000..eed05d59b --- /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 000000000..3a6a85d89 --- /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 000000000..eed05d59b --- /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 000000000..3a6a85d89 --- /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 000000000..eed05d59b --- /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 000000000..cb762eff8 --- /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 000000000..c7ab9ecff --- /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 000000000..3a6a85d89 --- /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 000000000..9eece75e5 --- /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 000000000..b64fa3b74 --- /dev/null +++ b/dom/security/test/sri/style3.css @@ -0,0 +1,3 @@ +#black-text { + color: green; +} diff --git a/dom/security/test/sri/style4.css b/dom/security/test/sri/style4.css new file mode 100644 index 000000000..eab83656e --- /dev/null +++ b/dom/security/test/sri/style4.css @@ -0,0 +1,4 @@ +/* François was here. */ +#purple-text { + color: purple; +} diff --git a/dom/security/test/sri/style4.css^headers^ b/dom/security/test/sri/style4.css^headers^ new file mode 100644 index 000000000..e13897f15 --- /dev/null +++ b/dom/security/test/sri/style4.css^headers^ @@ -0,0 +1 @@ +Content-Type: text/css; charset=utf-8 diff --git a/dom/security/test/sri/style5.css b/dom/security/test/sri/style5.css new file mode 100644 index 000000000..5d59134cc --- /dev/null +++ b/dom/security/test/sri/style5.css @@ -0,0 +1,4 @@ +/* François was here. */ +#orange-text { + color: orange; +} diff --git a/dom/security/test/sri/style6.css b/dom/security/test/sri/style6.css new file mode 100644 index 000000000..569557694 --- /dev/null +++ b/dom/security/test/sri/style6.css @@ -0,0 +1,4 @@ +/* François was here. */ +#brown-text { + color: brown; +} diff --git a/dom/security/test/sri/style6.css^headers^ b/dom/security/test/sri/style6.css^headers^ new file mode 100644 index 000000000..d866aa522 --- /dev/null +++ b/dom/security/test/sri/style6.css^headers^ @@ -0,0 +1 @@ +Content-Type: text/css; charset=iso-8859-8 diff --git a/dom/security/test/sri/style_301.css b/dom/security/test/sri/style_301.css new file mode 100644 index 000000000..c7ab9ecff --- /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 000000000..c5b78ee04 --- /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/style_imported.css b/dom/security/test/sri/style_imported.css new file mode 100644 index 000000000..4469e8d78 --- /dev/null +++ b/dom/security/test/sri/style_imported.css @@ -0,0 +1,6 @@ +#text-for-import-test { + color: red; +} +#text-for-import-test::before { + content: 'Test failed'; +} diff --git a/dom/security/test/sri/style_importing.css b/dom/security/test/sri/style_importing.css new file mode 100644 index 000000000..985dd3467 --- /dev/null +++ b/dom/security/test/sri/style_importing.css @@ -0,0 +1,4 @@ +/* neither of them should load. trying multiple cases*/ +@import url("style_imported.css"); +@import 'style_imported.css'; +#text-for-import-test { color: blue; } diff --git a/dom/security/test/sri/test_bug_1271796.html b/dom/security/test/sri/test_bug_1271796.html new file mode 100644 index 000000000..1d639c9cb --- /dev/null +++ b/dom/security/test/sri/test_bug_1271796.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + function good_shouldLoadEncodingProblem() { + ok(true, "Problematically encoded file correctly loaded.") + }; + function bad_shouldntEncounterBug1271796() { + ok(false, "Problematically encoded should load!") + } + window.onload = function() { + SimpleTest.finish(); + } + </script> + <link rel="stylesheet" href="file_bug_1271796.css" crossorigin="anonymous" + integrity="sha384-8Xl0mTN4S2QZ5xeliG1sd4Ar9o1xMw6JoJy9RNjyHGQDha7GiLxo8l1llwLVgTNG" + onload="good_shouldLoadEncodingProblem();" + onerror="bad_shouldntEncounterBug1271796();"> +</head> +<body> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1271796">Bug 1271796</a><br> +<p>This text is prepended by emdash if css has loaded</p> +</body> +</html> diff --git a/dom/security/test/sri/test_csp_directive_style_imports.html b/dom/security/test/sri/test_csp_directive_style_imports.html new file mode 100644 index 000000000..f293e2412 --- /dev/null +++ b/dom/security/test/sri/test_csp_directive_style_imports.html @@ -0,0 +1,42 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SRI require-sri-for CSP directive</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1265318">Mozilla Bug 1265318</a><br> +<iframe style="width:200px;height:200px;" id="test_frame"></iframe><br> +</body> +<script type="application/javascript"> + var finished = 0; + SpecialPowers.setBoolPref("security.csp.experimentalEnabled", true); + SimpleTest.waitForExplicitFinish(); + function handler(event) { + console.log(event); + switch (event.data) { + case 'finish': + // need finish message from iframe_require-sri-for_main onload event and + // from iframe_require-sri-for_no_csp, which spawns a Worker + var importText = frame.contentDocument.getElementById('text-for-import-test'); + var importColor = frame.contentWindow.getComputedStyle(importText, null).getPropertyValue('color'); + ok(importColor == 'rgb(0, 0, 255)', "The import should not work without integrity. The text is now red, but should not."); + removeEventListener('message', handler); + SimpleTest.finish(); + break; + default: + ok(false, 'Something is wrong here'); + break; + } + } + addEventListener("message", handler); + // This frame has a CSP that requires SRI + var frame = document.getElementById("test_frame"); + frame.src = "iframe_csp_directive_style_imports.html"; +</script> +</html> diff --git a/dom/security/test/sri/test_require-sri-for_csp_directive.html b/dom/security/test/sri/test_require-sri-for_csp_directive.html new file mode 100644 index 000000000..ef1b3603f --- /dev/null +++ b/dom/security/test/sri/test_require-sri-for_csp_directive.html @@ -0,0 +1,76 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SRI require-sri-for CSP directive</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1265318">Mozilla Bug 1265318</a><br> +<iframe style="width:200px;height:200px;" id="test_frame"></iframe><br> +<iframe style="width:200px;height:200px;" id="test_frame_no_csp"></iframe> +</body> +<script type="application/javascript"> + var finished = 0; + SpecialPowers.setBoolPref("security.csp.experimentalEnabled", true); + SimpleTest.waitForExplicitFinish(); + function handler(event) { + switch (event.data) { + case 'good_sriLoaded': + ok(true, "Eligible SRI resources was correctly loaded."); + break; + case 'bad_nonsriLoaded': + ok(false, "Eligible non-SRI resource should be blocked by the CSP!"); + break; + case 'good_nonsriBlocked': + ok(true, "Eligible non-SRI resources was correctly blocked by the CSP."); + break; + case 'bad_svg_nonsriLoaded': + ok(false, 'Eligible non-SRI resource should be blocked by the CSP.'); + break; + case 'good_svg_nonsriBlocked': + ok(true, 'Eligible non-SRI svg script was correctly blocked by the CSP.'); + break; + case 'bad_worker_could_load': + ok(false, 'require-sri-for failed to block loading a Worker with no integrity metadata.'); + break; + case 'good_worker_could_load': + ok(true, "Loaded a worker that has require-sri-for set (but its parent doesnt).") + break; + case 'bad_worker_could_load_via_importScripts': + ok(false, 'require-sri-for failed to block loading importScript in a worker though we require SRI via CSP'); + break; + case 'good_worker_after_importscripts': + ok(true, 'Worker continued after failed importScript due to require-sri-for'); + break; + case 'finish': + finished++; + if (finished > 1) { + // need finish message from iframe_require-sri-for_main onload event and + // from iframe_require-sri-for_no_csp, which spawns a Worker + var blackText = frame.contentDocument.getElementById('black-text'); + var blackTextColor = frame.contentWindow.getComputedStyle(blackText, null).getPropertyValue('color'); + ok(blackTextColor == 'rgb(0, 0, 0)', "The second part should not be black."); + removeEventListener('message', handler); + SimpleTest.finish(); + } + break; + default: + ok(false, 'Something is wrong here'); + break; + } + } + addEventListener("message", handler); + // This frame has a CSP that requires SRI + var frame = document.getElementById("test_frame"); + frame.src = "iframe_require-sri-for_main.html"; + // This frame has no CSP to require SRI. + // Used for testing require-sri-for in a Worker. + var frame_no_csp = document.getElementById("test_frame_no_csp"); + frame_no_csp.src = "iframe_require-sri-for_no_csp.html"; +</script> +</html> diff --git a/dom/security/test/sri/test_require-sri-for_csp_directive_disabled.html b/dom/security/test/sri/test_require-sri-for_csp_directive_disabled.html new file mode 100644 index 000000000..b833fe11c --- /dev/null +++ b/dom/security/test/sri/test_require-sri-for_csp_directive_disabled.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for diabled SRI require-sri-for CSP directive</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1265318">Mozilla Bug 1265318</a> +<iframe style="width:200px;height:200px;" id="test_frame"></iframe> +</body> +<script type="application/javascript"> + SpecialPowers.setBoolPref("security.csp.experimentalEnabled", false); + SimpleTest.waitForExplicitFinish(); + function handler(event) { + switch (event.data) { + case 'good_sriLoaded': + ok(true, "Eligible SRI resources was correctly loaded."); + break; + case 'bad_nonsriLoaded': + ok(true, "Eligible non-SRI resource should be blocked by the CSP!"); + break; + case 'good_nonsriBlocked': + ok(false, "Eligible non-SRI resources was correctly blocked by the CSP."); + break; + case 'finish': + var blackText = frame.contentDocument.getElementById('black-text'); + var blackTextColor = frame.contentWindow.getComputedStyle(blackText, null).getPropertyValue('color'); + ok(blackTextColor != 'rgb(0, 0, 0)', "The second part should still be black."); + removeEventListener('message', handler); + SimpleTest.finish(); + break; + default: + ok(false, 'Something is wrong here'); + break; + } + } + addEventListener("message", handler); + var frame = document.getElementById("test_frame"); + frame.src = "iframe_require-sri-for_main.html"; +</script> +</html> 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 000000000..327ea4247 --- /dev/null +++ b/dom/security/test/sri/test_script_crossdomain.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <meta charset="utf-8"> + <title>Cross-domain script tests for Bug 992096</title> + <script> + SpecialPowers.setBoolPref("security.sri.enable", true); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=992096">Mozilla Bug 992096</a> +<div> + <iframe src="iframe_script_crossdomain.html" height="100%" width="90%" frameborder="0"></iframe> +</div> +</body> +</html> 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 000000000..df890d5f9 --- /dev/null +++ b/dom/security/test/sri/test_script_sameorigin.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <meta charset="utf-8"> + <title>Same-origin script tests for Bug 992096</title> + <script> + SpecialPowers.setBoolPref("security.sri.enable", true); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=992096">Mozilla Bug 992096</a> +<div> + <iframe src="iframe_script_sameorigin.html" height="100%" width="90%" frameborder="0"></iframe> +</div> +</body> +</html> 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 000000000..4661235b1 --- /dev/null +++ b/dom/security/test/sri/test_sri_disabled.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <meta charset="utf-8"> + <title>security.sri.enable tests for Bug 992096</title> + <script> + SpecialPowers.setBoolPref("security.sri.enable", false); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=992096">Mozilla Bug 992096</a> +<div> + <iframe src="iframe_sri_disabled.html" height="100%" width="90%" frameborder="0"></iframe> +</div> +</body> +</html> 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 000000000..6bf4b2de1 --- /dev/null +++ b/dom/security/test/sri/test_style_crossdomain.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <meta charset="utf-8"> + <title>Cross-domain stylesheet tests for Bug 1196740</title> + <script> + SpecialPowers.setBoolPref("security.sri.enable", true); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196740">Mozilla Bug 1196740</a> +<div> + <iframe src="iframe_style_crossdomain.html" height="100%" width="90%" frameborder="0"></iframe> +</div> +</body> +</html> 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 000000000..fb1d3f78d --- /dev/null +++ b/dom/security/test/sri/test_style_sameorigin.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <meta charset="utf-8"> + <title>Same-origin stylesheet tests for Bug 992096</title> + <script> + SpecialPowers.setBoolPref("security.sri.enable", true); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=992096">Mozilla Bug 992096</a> +<div> + <iframe src="iframe_style_sameorigin.html" height="100%" width="90%" frameborder="0"></iframe> +</div> +</body> +</html> diff --git a/dom/security/test/unit/test_csp_reports.js b/dom/security/test/unit/test_csp_reports.js new file mode 100644 index 000000000..6c88fb1e1 --- /dev/null +++ b/dom/security/test/unit/test_csp_reports.js @@ -0,0 +1,231 @@ +/* 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/. */ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +Cu.import('resource://gre/modules/NetUtil.jsm'); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://testing-common/httpd.js"); + +var httpServer = new HttpServer(); +httpServer.start(-1); +var testsToFinish = 0; + +var principal; + +const REPORT_SERVER_PORT = httpServer.identity.primaryPort; +const REPORT_SERVER_URI = "http://localhost"; +const REPORT_SERVER_PATH = "/report"; + +/** + * Construct a callback that listens to a report submission and either passes + * or fails a test based on what it gets. + */ +function makeReportHandler(testpath, message, expectedJSON) { + return function(request, response) { + // we only like "POST" submissions for reports! + if (request.method !== "POST") { + do_throw("violation report should be a POST request"); + return; + } + + // check content-type of report is "application/csp-report" + var contentType = request.hasHeader("Content-Type") + ? request.getHeader("Content-Type") : undefined; + if (contentType !== "application/csp-report") { + do_throw("violation report should have the 'application/csp-report' " + + "content-type, when in fact it is " + contentType.toString()) + } + + // obtain violation report + var reportObj = JSON.parse( + NetUtil.readInputStreamToString( + request.bodyInputStream, + request.bodyInputStream.available())); + + // dump("GOT REPORT:\n" + JSON.stringify(reportObj) + "\n"); + // dump("TESTPATH: " + testpath + "\n"); + // dump("EXPECTED: \n" + JSON.stringify(expectedJSON) + "\n\n"); + + for (var i in expectedJSON) + do_check_eq(expectedJSON[i], reportObj['csp-report'][i]); + + testsToFinish--; + httpServer.registerPathHandler(testpath, null); + if (testsToFinish < 1) + httpServer.stop(do_test_finished); + else + do_test_finished(); + }; +} + +/** + * Everything created by this assumes it will cause a report. If you want to + * add a test here that will *not* cause a report to go out, you're gonna have + * to make sure the test cleans up after itself. + */ +function makeTest(id, expectedJSON, useReportOnlyPolicy, callback) { + testsToFinish++; + do_test_pending(); + + // set up a new CSP instance for each test. + var csp = Cc["@mozilla.org/cspcontext;1"] + .createInstance(Ci.nsIContentSecurityPolicy); + var policy = "default-src 'none'; " + + "report-uri " + REPORT_SERVER_URI + + ":" + REPORT_SERVER_PORT + + "/test" + id; + var selfuri = NetUtil.newURI(REPORT_SERVER_URI + + ":" + REPORT_SERVER_PORT + + "/foo/self"); + + dump("Created test " + id + " : " + policy + "\n\n"); + + let ssm = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + principal = ssm.createCodebasePrincipal(selfuri, {}); + csp.setRequestContext(null, principal); + + // Load up the policy + // set as report-only if that's the case + csp.appendPolicy(policy, useReportOnlyPolicy, false); + + // prime the report server + var handler = makeReportHandler("/test" + id, "Test " + id, expectedJSON); + httpServer.registerPathHandler("/test" + id, handler); + + //trigger the violation + callback(csp); +} + +function run_test() { + var selfuri = NetUtil.newURI(REPORT_SERVER_URI + + ":" + REPORT_SERVER_PORT + + "/foo/self"); + + // test that inline script violations cause a report. + makeTest(0, {"blocked-uri": "self"}, false, + function(csp) { + let inlineOK = true; + inlineOK = csp.getAllowsInline(Ci.nsIContentPolicy.TYPE_SCRIPT, + "", // aNonce + false, // aParserCreated + "", // aContent + 0); // aLineNumber + + // this is not a report only policy, so it better block inline scripts + do_check_false(inlineOK); + }); + + // test that eval violations cause a report. + makeTest(1, {"blocked-uri": "self", + // JSON script-sample is UTF8 encoded + "script-sample" : "\xc2\xa3\xc2\xa5\xc2\xb5\xe5\x8c\x97\xf0\xa0\x9d\xb9"}, false, + function(csp) { + let evalOK = true, oReportViolation = {'value': false}; + evalOK = csp.getAllowsEval(oReportViolation); + + // this is not a report only policy, so it better block eval + do_check_false(evalOK); + // ... and cause reports to go out + do_check_true(oReportViolation.value); + + if (oReportViolation.value) { + // force the logging, since the getter doesn't. + csp.logViolationDetails(Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_EVAL, + selfuri.asciiSpec, + // sending UTF-16 script sample to make sure + // csp report in JSON is not cut-off, please + // note that JSON is UTF8 encoded. + "\u00a3\u00a5\u00b5\u5317\ud841\udf79", + 1); + } + }); + + makeTest(2, {"blocked-uri": "http://blocked.test"}, false, + function(csp) { + // shouldLoad creates and sends out the report here. + csp.shouldLoad(Ci.nsIContentPolicy.TYPE_SCRIPT, + NetUtil.newURI("http://blocked.test/foo.js"), + null, null, null, null); + }); + + // test that inline script violations cause a report in report-only policy + makeTest(3, {"blocked-uri": "self"}, true, + function(csp) { + let inlineOK = true; + inlineOK = csp.getAllowsInline(Ci.nsIContentPolicy.TYPE_SCRIPT, + "", // aNonce + false, // aParserCreated + "", // aContent + 0); // aLineNumber + + // this is a report only policy, so it better allow inline scripts + do_check_true(inlineOK); + }); + + // test that eval violations cause a report in report-only policy + makeTest(4, {"blocked-uri": "self"}, true, + function(csp) { + let evalOK = true, oReportViolation = {'value': false}; + evalOK = csp.getAllowsEval(oReportViolation); + + // this is a report only policy, so it better allow eval + do_check_true(evalOK); + // ... but still cause reports to go out + do_check_true(oReportViolation.value); + + if (oReportViolation.value) { + // force the logging, since the getter doesn't. + csp.logViolationDetails(Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_INLINE_SCRIPT, + selfuri.asciiSpec, + "script sample", + 4); + } + }); + + // test that only the uri's scheme is reported for globally unique identifiers + makeTest(5, {"blocked-uri": "data"}, false, + function(csp) { + var base64data = + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; + // shouldLoad creates and sends out the report here. + csp.shouldLoad(Ci.nsIContentPolicy.TYPE_IMAGE, + NetUtil.newURI("data:image/png;base64," + base64data), + null, null, null, null); + }); + + // test that only the uri's scheme is reported for globally unique identifiers + makeTest(6, {"blocked-uri": "intent"}, false, + function(csp) { + // shouldLoad creates and sends out the report here. + csp.shouldLoad(Ci.nsIContentPolicy.TYPE_SUBDOCUMENT, + NetUtil.newURI("intent://mymaps.com/maps?um=1&ie=UTF-8&fb=1&sll"), + null, null, null, null); + }); + + // test fragment removal + var selfSpec = REPORT_SERVER_URI + ":" + REPORT_SERVER_PORT + "/foo/self/foo.js"; + makeTest(7, {"blocked-uri": selfSpec}, false, + function(csp) { + var uri = NetUtil + // shouldLoad creates and sends out the report here. + csp.shouldLoad(Ci.nsIContentPolicy.TYPE_SCRIPT, + NetUtil.newURI(selfSpec + "#bar"), + null, null, null, null); + }); + + // test scheme of ftp: + makeTest(8, {"blocked-uri": "ftp://blocked.test"}, false, + function(csp) { + // shouldLoad creates and sends out the report here. + csp.shouldLoad(Ci.nsIContentPolicy.TYPE_SCRIPT, + NetUtil.newURI("ftp://blocked.test/profile.png"), + null, null, null, null); + }); +} diff --git a/dom/security/test/unit/test_csp_upgrade_insecure_request_header.js b/dom/security/test/unit/test_csp_upgrade_insecure_request_header.js new file mode 100644 index 000000000..8be873234 --- /dev/null +++ b/dom/security/test/unit/test_csp_upgrade_insecure_request_header.js @@ -0,0 +1,107 @@ +var Cu = Components.utils; +var Ci = Components.interfaces; +var Cc = Components.classes; + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + +// Since this test creates a TYPE_DOCUMENT channel via javascript, it will +// end up using the wrong LoadInfo constructor. Setting this pref will disable +// the ContentPolicyType assertion in the constructor. +prefs.setBoolPref("network.loadinfo.skip_type_assertion", true); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = null; +var channel = null; +var curTest = null; +var testpath = "/footpath"; + +var tests = [ + { + description: "should not set request header for TYPE_OTHER", + expectingHeader: false, + contentType: Ci.nsIContentPolicy.TYPE_OTHER + }, + { + description: "should set request header for TYPE_DOCUMENT", + expectingHeader: true, + contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT + }, + { + description: "should set request header for TYPE_SUBDOCUMENT", + expectingHeader: true, + contentType: Ci.nsIContentPolicy.TYPE_SUBDOCUMENT + }, + { + description: "should not set request header for TYPE_IMG", + expectingHeader: false, + contentType: Ci.nsIContentPolicy.TYPE_IMG + }, +]; + +function ChannelListener() { +} + +ChannelListener.prototype = { + onStartRequest: function(request, context) { }, + onDataAvailable: function(request, context, stream, offset, count) { + do_throw("Should not get any data!"); + }, + onStopRequest: function(request, context, status) { + var upgrade_insecure_header = false; + try { + if (request.getRequestHeader("Upgrade-Insecure-Requests")) { + upgrade_insecure_header = true; + } + } + catch (e) { + // exception is thrown if header is not available on the request + } + // debug + // dump("executing test: " + curTest.description); + do_check_eq(upgrade_insecure_header, curTest.expectingHeader) + run_next_test(); + }, +}; + +function setupChannel(aContentType) { + var chan = NetUtil.newChannel({ + uri: URL + testpath, + loadUsingSystemPrincipal: true, + contentPolicyType: aContentType + }); + chan.QueryInterface(Ci.nsIHttpChannel); + chan.requestMethod = "GET"; + return chan; +} + +function serverHandler(metadata, response) { + // no need to perform anything here +} + +function run_next_test() { + curTest = tests.shift(); + if (!curTest) { + httpserver.stop(do_test_finished); + return; + } + channel = setupChannel(curTest.contentType); + channel.asyncOpen2(new ChannelListener()); +} + +function run_test() { + // set up the test environment + httpserver = new HttpServer(); + httpserver.registerPathHandler(testpath, serverHandler); + httpserver.start(-1); + + run_next_test(); + do_test_pending(); +} diff --git a/dom/security/test/unit/test_isOriginPotentiallyTrustworthy.js b/dom/security/test/unit/test_isOriginPotentiallyTrustworthy.js new file mode 100644 index 000000000..7de8faa8f --- /dev/null +++ b/dom/security/test/unit/test_isOriginPotentiallyTrustworthy.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests the "Is origin potentially trustworthy?" logic from + * <https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy>. + */ + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "gScriptSecurityManager", + "@mozilla.org/scriptsecuritymanager;1", + "nsIScriptSecurityManager"); + +XPCOMUtils.defineLazyServiceGetter(this, "gContentSecurityManager", + "@mozilla.org/contentsecuritymanager;1", + "nsIContentSecurityManager"); + +var prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); +prefs.setCharPref("dom.securecontext.whitelist", "example.net,example.org"); + +add_task(function* test_isOriginPotentiallyTrustworthy() { + for (let [uriSpec, expectedResult] of [ + ["http://example.com/", false], + ["https://example.com/", true], + ["http://localhost/", true], + ["http://127.0.0.1/", true], + ["file:///", true], + ["resource:///", true], + ["app://", true], + ["moz-extension://", true], + ["wss://example.com/", true], + ["about:config", false], + ["urn:generic", false], + ["http://example.net/", true], + ["ws://example.org/", true], + ["chrome://example.net/content/messenger.xul", false], + ]) { + let uri = NetUtil.newURI(uriSpec); + let principal = gScriptSecurityManager.getCodebasePrincipal(uri); + Assert.equal(gContentSecurityManager.isOriginPotentiallyTrustworthy(principal), + expectedResult); + } +}); diff --git a/dom/security/test/unit/xpcshell.ini b/dom/security/test/unit/xpcshell.ini new file mode 100644 index 000000000..7e1d4a0ed --- /dev/null +++ b/dom/security/test/unit/xpcshell.ini @@ -0,0 +1,7 @@ +[DEFAULT] +head = +tail = + +[test_csp_reports.js] +[test_isOriginPotentiallyTrustworthy.js] +[test_csp_upgrade_insecure_request_header.js] |