diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /dom/security | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/security')
834 files changed, 64592 insertions, 0 deletions
diff --git a/dom/security/CSPEvalChecker.cpp b/dom/security/CSPEvalChecker.cpp new file mode 100644 index 0000000000..8b46b96cd3 --- /dev/null +++ b/dom/security/CSPEvalChecker.cpp @@ -0,0 +1,190 @@ +/* -*- 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/dom/CSPEvalChecker.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ErrorResult.h" +#include "nsIParentChannel.h" +#include "nsGlobalWindowInner.h" +#include "nsContentSecurityUtils.h" +#include "nsContentUtils.h" +#include "nsCOMPtr.h" +#include "nsJSUtils.h" + +using namespace mozilla; +using namespace mozilla::dom; + +namespace { + +// We use the subjectPrincipal to assert that eval() is never +// executed in system privileged context. +nsresult CheckInternal(nsIContentSecurityPolicy* aCSP, + nsICSPEventListener* aCSPEventListener, + nsIPrincipal* aSubjectPrincipal, + const nsAString& aExpression, + const nsAString& aFileNameString, uint32_t aLineNum, + uint32_t aColumnNum, bool* aAllowed) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aAllowed); + + // The value is set at any "return", but better to have a default value here. + *aAllowed = false; + + // This is the non-CSP check for gating eval() use in the SystemPrincipal +#if !defined(ANDROID) + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (!nsContentSecurityUtils::IsEvalAllowed( + cx, aSubjectPrincipal->IsSystemPrincipal(), aExpression)) { + *aAllowed = false; + return NS_OK; + } +#endif + + if (!aCSP) { + *aAllowed = true; + return NS_OK; + } + + bool reportViolation = false; + nsresult rv = aCSP->GetAllowsEval(&reportViolation, aAllowed); + if (NS_WARN_IF(NS_FAILED(rv))) { + *aAllowed = false; + return rv; + } + + if (reportViolation) { + aCSP->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL, + nullptr, // triggering element + aCSPEventListener, aFileNameString, aExpression, + aLineNum, aColumnNum, u""_ns, u""_ns); + } + + return NS_OK; +} + +class WorkerCSPCheckRunnable final : public WorkerMainThreadRunnable { + public: + WorkerCSPCheckRunnable(WorkerPrivate* aWorkerPrivate, + const nsAString& aExpression, + const nsAString& aFileNameString, uint32_t aLineNum, + uint32_t aColumnNum) + : WorkerMainThreadRunnable(aWorkerPrivate, "CSP Eval Check"_ns), + mExpression(aExpression), + mFileNameString(aFileNameString), + mLineNum(aLineNum), + mColumnNum(aColumnNum), + mEvalAllowed(false) {} + + bool MainThreadRun() override { + mResult = CheckInternal( + mWorkerPrivate->GetCsp(), mWorkerPrivate->CSPEventListener(), + mWorkerPrivate->GetLoadingPrincipal(), mExpression, mFileNameString, + mLineNum, mColumnNum, &mEvalAllowed); + return true; + } + + nsresult GetResult(bool* aAllowed) { + MOZ_ASSERT(aAllowed); + *aAllowed = mEvalAllowed; + return mResult; + } + + private: + const nsString mExpression; + const nsString mFileNameString; + const uint32_t mLineNum; + const uint32_t mColumnNum; + bool mEvalAllowed; + nsresult mResult; +}; + +} // namespace + +/* static */ +nsresult CSPEvalChecker::CheckForWindow(JSContext* aCx, + nsGlobalWindowInner* aWindow, + const nsAString& aExpression, + bool* aAllowEval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aAllowEval); + + // The value is set at any "return", but better to have a default value here. + *aAllowEval = false; + + // if CSP is enabled, and setTimeout/setInterval was called with a string, + // disable the registration and log an error + nsCOMPtr<Document> doc = aWindow->GetExtantDoc(); + if (!doc) { + // if there's no document, we don't have to do anything. + *aAllowEval = true; + return NS_OK; + } + + nsresult rv = NS_OK; + + // Get the calling location. + uint32_t lineNum = 0; + uint32_t columnNum = 0; + nsAutoString fileNameString; + if (!nsJSUtils::GetCallingLocation(aCx, fileNameString, &lineNum, + &columnNum)) { + fileNameString.AssignLiteral("unknown"); + } + + nsCOMPtr<nsIContentSecurityPolicy> csp = doc->GetCsp(); + rv = CheckInternal(csp, nullptr /* no CSPEventListener for window */, + doc->NodePrincipal(), aExpression, fileNameString, lineNum, + columnNum, aAllowEval); + if (NS_WARN_IF(NS_FAILED(rv))) { + *aAllowEval = false; + return rv; + } + + return NS_OK; +} + +/* static */ +nsresult CSPEvalChecker::CheckForWorker(JSContext* aCx, + WorkerPrivate* aWorkerPrivate, + const nsAString& aExpression, + bool* aAllowEval) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aAllowEval); + + // The value is set at any "return", but better to have a default value here. + *aAllowEval = false; + + // Get the calling location. + uint32_t lineNum = 0; + uint32_t columnNum = 0; + nsAutoString fileNameString; + if (!nsJSUtils::GetCallingLocation(aCx, fileNameString, &lineNum, + &columnNum)) { + fileNameString.AssignLiteral("unknown"); + } + + RefPtr<WorkerCSPCheckRunnable> r = new WorkerCSPCheckRunnable( + aWorkerPrivate, aExpression, fileNameString, lineNum, columnNum); + ErrorResult error; + r->Dispatch(Canceling, error); + if (NS_WARN_IF(error.Failed())) { + *aAllowEval = false; + return error.StealNSResult(); + } + + nsresult rv = r->GetResult(aAllowEval); + if (NS_WARN_IF(NS_FAILED(rv))) { + *aAllowEval = false; + return rv; + } + + return NS_OK; +} diff --git a/dom/security/CSPEvalChecker.h b/dom/security/CSPEvalChecker.h new file mode 100644 index 0000000000..c2a1f8d774 --- /dev/null +++ b/dom/security/CSPEvalChecker.h @@ -0,0 +1,32 @@ +/* -*- 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_CSPEvalChecker_h +#define mozilla_dom_CSPEvalChecker_h + +#include "nsString.h" + +struct JSContext; +class nsGlobalWindowInner; + +namespace mozilla::dom { + +class WorkerPrivate; + +class CSPEvalChecker final { + public: + static nsresult CheckForWindow(JSContext* aCx, nsGlobalWindowInner* aWindow, + const nsAString& aExpression, + bool* aAllowEval); + + static nsresult CheckForWorker(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + const nsAString& aExpression, + bool* aAllowEval); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_CSPEvalChecker_h diff --git a/dom/security/DOMSecurityMonitor.cpp b/dom/security/DOMSecurityMonitor.cpp new file mode 100644 index 0000000000..70eb7f9e9a --- /dev/null +++ b/dom/security/DOMSecurityMonitor.cpp @@ -0,0 +1,136 @@ +/* -*- 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 "DOMSecurityMonitor.h" +#include "nsContentUtils.h" + +#include "nsIChannel.h" +#include "nsILoadInfo.h" +#include "nsIPrincipal.h" +#include "nsIURI.h" +#include "nsJSUtils.h" +#include "xpcpublic.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/StaticPrefs_dom.h" + +/* static */ +void DOMSecurityMonitor::AuditParsingOfHTMLXMLFragments( + nsIPrincipal* aPrincipal, const nsAString& aFragment) { + // if the fragment parser (e.g. innerHTML()) is not called in chrome: code + // or any of our about: pages, then there is nothing to do here. + if (!aPrincipal->IsSystemPrincipal() && !aPrincipal->SchemeIs("about")) { + return; + } + + // check if the fragment is empty, if so, we can return early. + if (aFragment.IsEmpty()) { + return; + } + + // check if there is a JS caller, if not, then we can can return early here + // because we only care about calls to the fragment parser (e.g. innerHTML) + // originating from JS code. + nsAutoString filename; + uint32_t lineNum = 0; + uint32_t columnNum = 0; + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (!cx || + !nsJSUtils::GetCallingLocation(cx, filename, &lineNum, &columnNum)) { + return; + } + + // check if we should skip assertion. Please only ever set this pref to + // true if really needed for testing purposes. + if (mozilla::StaticPrefs::dom_security_skip_html_fragment_assertion()) { + return; + } + + /* + * WARNING: Do not add any new entries to the htmlFragmentAllowlist + * without proper review from a dom:security peer! + */ + static nsLiteralCString htmlFragmentAllowlist[] = { + "chrome://global/content/elements/marquee.js"_ns, + nsLiteralCString( + "chrome://pocket/content/panels/js/vendor/jquery-2.1.1.min.js"), + nsLiteralCString("chrome://devtools/content/shared/sourceeditor/" + "codemirror/codemirror.bundle.js"), + nsLiteralCString( + "resource://activity-stream/data/content/activity-stream.bundle.js"), + nsLiteralCString("resource://devtools/client/debugger/src/components/" + "Editor/Breakpoint.js"), + nsLiteralCString("resource://devtools/client/debugger/src/components/" + "Editor/ColumnBreakpoint.js"), + nsLiteralCString( + "resource://devtools/client/shared/vendor/fluent-react.js"), + "resource://devtools/client/shared/vendor/react-dom.js"_ns, + nsLiteralCString( + "resource://devtools/client/shared/vendor/react-dom-dev.js"), + nsLiteralCString( + "resource://devtools/client/shared/widgets/FilterWidget.js"), + nsLiteralCString("resource://devtools/client/shared/widgets/tooltip/" + "inactive-css-tooltip-helper.js"), + "resource://devtools/client/shared/widgets/Spectrum.js"_ns, + "resource://gre/modules/narrate/VoiceSelect.jsm"_ns, + "resource://normandy-vendor/ReactDOM.js"_ns, + // ------------------------------------------------------------------ + // test pages + // ------------------------------------------------------------------ + "chrome://mochikit/content/browser-harness.xhtml"_ns, + "chrome://mochikit/content/harness.xhtml"_ns, + "chrome://mochikit/content/tests/"_ns, + "chrome://mochitests/content/"_ns, + "chrome://reftest/content/"_ns, + }; + + for (const nsLiteralCString& allowlistEntry : htmlFragmentAllowlist) { + if (StringBeginsWith(NS_ConvertUTF16toUTF8(filename), allowlistEntry)) { + return; + } + } + + nsAutoCString uriSpec; + aPrincipal->GetAsciiSpec(uriSpec); + + // Ideally we should not call the fragment parser (e.g. innerHTML()) in + // chrome: code or any of our about: pages. If you hit that assertion, + // please do *not* add your filename to the allowlist above, but rather + // refactor your code. + fprintf(stderr, + "Do not call the fragment parser (e.g innerHTML()) in chrome code " + "or in about: pages, (uri: %s), (caller: %s, line: %d, col: %d), " + "(fragment: %s)", + uriSpec.get(), NS_ConvertUTF16toUTF8(filename).get(), lineNum, + columnNum, NS_ConvertUTF16toUTF8(aFragment).get()); + + xpc_DumpJSStack(true, true, false); + MOZ_ASSERT(false); +} + +/* static */ +void DOMSecurityMonitor::AuditUseOfJavaScriptURI(nsIChannel* aChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + nsCOMPtr<nsIPrincipal> loadingPrincipal = loadInfo->GetLoadingPrincipal(); + + // We only ever have no loadingPrincipal in case of a new top-level load. + // The purpose of this assertion is to make sure we do not allow loading + // javascript: URIs in system privileged contexts. Hence there is nothing + // to do here in case there is no loadingPrincipal. + if (!loadingPrincipal) { + return; + } + + // if the javascript: URI is not loaded by a system privileged context + // or an about: page, there there is nothing to do here. + if (!loadingPrincipal->IsSystemPrincipal() && + !loadingPrincipal->SchemeIs("about")) { + return; + } + + MOZ_ASSERT(false, + "Do not use javascript: URIs in chrome code or in about: pages"); +} diff --git a/dom/security/DOMSecurityMonitor.h b/dom/security/DOMSecurityMonitor.h new file mode 100644 index 0000000000..457e8a143a --- /dev/null +++ b/dom/security/DOMSecurityMonitor.h @@ -0,0 +1,43 @@ +/* -*- 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_DOMSecurityMonitor_h +#define mozilla_dom_DOMSecurityMonitor_h + +#include "nsStringFwd.h" + +class nsIChannel; +class nsIPrincipal; + +class DOMSecurityMonitor final { + public: + /* The fragment parser is triggered anytime JS calls innerHTML or similar + * JS functions which can generate HTML fragments. This generation of + * HTML might be dangerous, hence we should ensure that no new instances + * of innerHTML and similar functions are introduced in system privileged + * contexts, or also about: pages, in our codebase. + * + * If the auditor detects a new instance of innerHTML or similar + * function it will CRASH using a strong assertion. + */ + static void AuditParsingOfHTMLXMLFragments(nsIPrincipal* aPrincipal, + const nsAString& aFragment); + + /* The use of javascript: URIs in system privileged contexts or + * also about: pages is considered unsafe and discouraged. + * + * If the auditor detects a javascript: URI in a privileged + * context it will CRASH using a strong assertion. + * + */ + static void AuditUseOfJavaScriptURI(nsIChannel* aChannel); + + private: + DOMSecurityMonitor() = default; + ~DOMSecurityMonitor() = default; +}; + +#endif /* mozilla_dom_DOMSecurityMonitor_h */ diff --git a/dom/security/FramingChecker.cpp b/dom/security/FramingChecker.cpp new file mode 100644 index 0000000000..be82ac9bb3 --- /dev/null +++ b/dom/security/FramingChecker.cpp @@ -0,0 +1,244 @@ +/* -*- 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 "FramingChecker.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsContentUtils.h" +#include "nsCSPUtils.h" +#include "nsDocShell.h" +#include "nsHttpChannel.h" +#include "nsContentSecurityUtils.h" +#include "nsGlobalWindowOuter.h" +#include "nsIChannel.h" +#include "nsIConsoleReportCollector.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIScriptError.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" +#include "nsTArray.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/nsCSPUtils.h" +#include "mozilla/dom/LoadURIOptionsBinding.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/net/HttpBaseChannel.h" + +#include "nsIObserverService.h" + +using namespace mozilla; +using namespace mozilla::dom; + +/* static */ +void FramingChecker::ReportError(const char* aMessageTag, + nsIHttpChannel* aChannel, nsIURI* aURI, + const nsAString& aPolicy) { + MOZ_ASSERT(aChannel); + MOZ_ASSERT(aURI); + + nsCOMPtr<net::HttpBaseChannel> httpChannel = do_QueryInterface(aChannel); + if (!httpChannel) { + return; + } + + // Get the URL spec + nsAutoCString spec; + nsresult rv = aURI->GetAsciiSpec(spec); + if (NS_FAILED(rv)) { + return; + } + + nsTArray<nsString> params; + params.AppendElement(aPolicy); + params.AppendElement(NS_ConvertUTF8toUTF16(spec)); + + httpChannel->AddConsoleReport(nsIScriptError::errorFlag, "X-Frame-Options"_ns, + nsContentUtils::eSECURITY_PROPERTIES, spec, 0, + 0, nsDependentCString(aMessageTag), params); +} + +/* static */ +bool FramingChecker::CheckOneFrameOptionsPolicy(nsIHttpChannel* aHttpChannel, + const nsAString& aPolicy) { + nsCOMPtr<nsIURI> uri; + aHttpChannel->GetURI(getter_AddRefs(uri)); + + // return early if header does not have one of the values with meaning + if (!aPolicy.LowerCaseEqualsLiteral("deny") && + !aPolicy.LowerCaseEqualsLiteral("sameorigin")) { + ReportError("XFrameOptionsInvalid", aHttpChannel, uri, aPolicy); + return true; + } + + // If the X-Frame-Options value is SAMEORIGIN, then the top frame in the + // parent chain must be from the same origin as this document. + bool checkSameOrigin = aPolicy.LowerCaseEqualsLiteral("sameorigin"); + + nsCOMPtr<nsILoadInfo> loadInfo = aHttpChannel->LoadInfo(); + RefPtr<mozilla::dom::BrowsingContext> ctx; + loadInfo->GetBrowsingContext(getter_AddRefs(ctx)); + + while (ctx) { + nsCOMPtr<nsIPrincipal> principal; + // Generally CheckOneFrameOptionsPolicy is consulted from within the + // DocumentLoadListener in the parent process. For loads of type object + // and embed it's called from the Document in the content process. + // After Bug 1646899 we should be able to remove that branching code for + // querying the principal. + if (XRE_IsParentProcess()) { + WindowGlobalParent* window = ctx->Canonical()->GetCurrentWindowGlobal(); + if (window) { + // Using the URI of the Principal and not the document because e.g. + // window.open inherits the principal and hence the URI of the + // opening context needed for same origin checks. + principal = window->DocumentPrincipal(); + } + } else if (nsPIDOMWindowOuter* windowOuter = ctx->GetDOMWindow()) { + principal = nsGlobalWindowOuter::Cast(windowOuter)->GetPrincipal(); + } + + if (principal && principal->IsSystemPrincipal()) { + return true; + } + + if (checkSameOrigin) { + bool isSameOrigin = principal && principal->IsSameOrigin(uri); + // one of the ancestors is not same origin as this document + if (!isSameOrigin) { + ReportError("XFrameOptionsDeny", aHttpChannel, uri, aPolicy); + return false; + } + } + ctx = ctx->GetParent(); + } + + // If the value of the header is DENY, and the previous condition is + // not met (current docshell is not the top docshell), prohibit the + // load. + if (aPolicy.LowerCaseEqualsLiteral("deny")) { + ReportError("XFrameOptionsDeny", aHttpChannel, uri, aPolicy); + return false; + } + + return true; +} + +// Ignore x-frame-options if CSP with frame-ancestors exists +static bool ShouldIgnoreFrameOptions(nsIChannel* aChannel, + nsIContentSecurityPolicy* aCSP) { + NS_ENSURE_TRUE(aChannel, false); + if (!aCSP) { + return false; + } + + bool enforcesFrameAncestors = false; + aCSP->GetEnforcesFrameAncestors(&enforcesFrameAncestors); + if (!enforcesFrameAncestors) { + // if CSP does not contain frame-ancestors, then there + // is nothing to do here. + return false; + } + + // log warning to console that xfo is ignored because of CSP + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + uint64_t innerWindowID = loadInfo->GetInnerWindowID(); + bool privateWindow = !!loadInfo->GetOriginAttributes().mPrivateBrowsingId; + AutoTArray<nsString, 2> params = {u"x-frame-options"_ns, + u"frame-ancestors"_ns}; + CSP_LogLocalizedStr("IgnoringSrcBecauseOfDirective", params, + u""_ns, // no sourcefile + u""_ns, // no scriptsample + 0, // no linenumber + 0, // no columnnumber + nsIScriptError::warningFlag, + "IgnoringSrcBecauseOfDirective"_ns, innerWindowID, + privateWindow); + + return true; +} + +// Check if X-Frame-Options permits this document to be loaded as a +// subdocument. This will iterate through and check any number of +// X-Frame-Options policies in the request (comma-separated in a header, +// multiple headers, etc). +/* static */ +bool FramingChecker::CheckFrameOptions(nsIChannel* aChannel, + nsIContentSecurityPolicy* aCsp) { + if (!aChannel) { + return true; + } + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + ExtContentPolicyType contentType = loadInfo->GetExternalContentPolicyType(); + + // xfo check only makes sense for subdocument and object loads, if this is + // not a load of such type, there is nothing to do here. + if (contentType != ExtContentPolicy::TYPE_SUBDOCUMENT && + contentType != ExtContentPolicy::TYPE_OBJECT) { + return true; + } + + nsCOMPtr<nsIHttpChannel> httpChannel; + nsresult rv = nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart( + aChannel, getter_AddRefs(httpChannel)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return true; + } + + // xfo can only hang off an httpchannel, if this is not an httpChannel + // then there is nothing to do here. + if (!httpChannel) { + return true; + } + + // ignore XFO checks on channels that will be redirected + uint32_t responseStatus; + rv = httpChannel->GetResponseStatus(&responseStatus); + // GetResponseStatus returning failure is expected in several situations, so + // do not warn if it fails. + if (NS_FAILED(rv)) { + return true; + } + if (mozilla::net::nsHttpChannel::IsRedirectStatus(responseStatus)) { + return true; + } + + nsAutoCString xfoHeaderCValue; + Unused << httpChannel->GetResponseHeader("X-Frame-Options"_ns, + xfoHeaderCValue); + NS_ConvertUTF8toUTF16 xfoHeaderValue(xfoHeaderCValue); + + // if no header value, there's nothing to do. + if (xfoHeaderValue.IsEmpty()) { + return true; + } + + // xfo checks are ignored in case CSP frame-ancestors is present, + // if so, there is nothing to do here. + if (ShouldIgnoreFrameOptions(aChannel, aCsp)) { + return true; + } + + // iterate through all the header values (usually there's only one, but can + // be many. If any want to deny the load, deny the load. + nsCharSeparatedTokenizer tokenizer(xfoHeaderValue, ','); + while (tokenizer.hasMoreTokens()) { + const nsAString& tok = tokenizer.nextToken(); + if (!CheckOneFrameOptionsPolicy(httpChannel, tok)) { + // if xfo blocks the load we are notifying observers for + // testing purposes because there is no event to gather + // what an iframe load was blocked or not. + nsCOMPtr<nsIURI> uri; + httpChannel->GetURI(getter_AddRefs(uri)); + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + nsAutoString policy(tok); + observerService->NotifyObservers(uri, "xfo-on-violate-policy", + policy.get()); + return false; + } + } + return true; +} diff --git a/dom/security/FramingChecker.h b/dom/security/FramingChecker.h new file mode 100644 index 0000000000..17f0bef82f --- /dev/null +++ b/dom/security/FramingChecker.h @@ -0,0 +1,48 @@ +/* -*- 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_FramingChecker_h +#define mozilla_dom_FramingChecker_h + +#include "nsStringFwd.h" + +class nsIDocShell; +class nsIChannel; +class nsIHttpChannel; +class nsIDocShellTreeItem; +class nsIURI; +class nsIContentSecurityPolicy; + +namespace mozilla::dom { +class BrowsingContext; +} // namespace mozilla::dom + +class FramingChecker { + public: + // Determine if X-Frame-Options allows content to be framed + // as a subdocument + static bool CheckFrameOptions(nsIChannel* aChannel, + nsIContentSecurityPolicy* aCSP); + + protected: + enum XFOHeader { eDENY, eSAMEORIGIN }; + + /** + * Logs to the window about a X-Frame-Options error. + * + * @param aMessageTag the error message identifier to log + * @param aChannel the HTTP Channel + * @param aURI the URI of the frame attempting to load + * @param aPolicy the header value string from the frame to the console. + */ + static void ReportError(const char* aMessageTag, nsIHttpChannel* aChannel, + nsIURI* aURI, const nsAString& aPolicy); + + static bool CheckOneFrameOptionsPolicy(nsIHttpChannel* aHttpChannel, + const nsAString& aPolicy); +}; + +#endif /* mozilla_dom_FramingChecker_h */ diff --git a/dom/security/PolicyTokenizer.cpp b/dom/security/PolicyTokenizer.cpp new file mode 100644 index 0000000000..8e28e6ce32 --- /dev/null +++ b/dom/security/PolicyTokenizer.cpp @@ -0,0 +1,70 @@ +/* -*- 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 "PolicyTokenizer.h" + +#include "mozilla/Logging.h" + +static mozilla::LogModule* GetPolicyTokenizerLog() { + static mozilla::LazyLogModule gPolicyTokenizerPRLog("PolicyTokenizer"); + return gPolicyTokenizerPRLog; +} + +#define POLICYTOKENIZERLOG(args) \ + MOZ_LOG(GetPolicyTokenizerLog(), mozilla::LogLevel::Debug, args) + +static const char16_t SEMICOL = ';'; + +PolicyTokenizer::PolicyTokenizer(const char16_t* aStart, const char16_t* aEnd) + : mCurChar(aStart), mEndChar(aEnd) { + POLICYTOKENIZERLOG(("PolicyTokenizer::PolicyTokenizer")); +} + +PolicyTokenizer::~PolicyTokenizer() { + POLICYTOKENIZERLOG(("PolicyTokenizer::~PolicyTokenizer")); +} + +void PolicyTokenizer::generateNextToken() { + skipWhiteSpaceAndSemicolon(); + MOZ_ASSERT(mCurToken.Length() == 0); + const char16_t* const start = mCurChar; + while (!atEnd() && !nsContentUtils::IsHTMLWhitespace(*mCurChar) && + *mCurChar != SEMICOL) { + mCurChar++; + } + if (start != mCurChar) { + mCurToken.Append(start, mCurChar - start); + } + POLICYTOKENIZERLOG(("PolicyTokenizer::generateNextToken: %s", + NS_ConvertUTF16toUTF8(mCurToken).get())); +} + +void PolicyTokenizer::generateTokens(policyTokens& outTokens) { + POLICYTOKENIZERLOG(("PolicyTokenizer::generateTokens")); + + // dirAndSrcs holds one set of [ name, src, src, src, ... ] + nsTArray<nsString> dirAndSrcs; + + while (!atEnd()) { + generateNextToken(); + dirAndSrcs.AppendElement(mCurToken); + skipWhiteSpace(); + if (atEnd() || accept(SEMICOL)) { + outTokens.AppendElement(std::move(dirAndSrcs)); + dirAndSrcs.ClearAndRetainStorage(); + } + } +} + +void PolicyTokenizer::tokenizePolicy(const nsAString& aPolicyString, + policyTokens& outTokens) { + POLICYTOKENIZERLOG(("PolicyTokenizer::tokenizePolicy")); + + PolicyTokenizer tokenizer(aPolicyString.BeginReading(), + aPolicyString.EndReading()); + + tokenizer.generateTokens(outTokens); +} diff --git a/dom/security/PolicyTokenizer.h b/dom/security/PolicyTokenizer.h new file mode 100644 index 0000000000..1c3c18175d --- /dev/null +++ b/dom/security/PolicyTokenizer.h @@ -0,0 +1,78 @@ +/* -*- 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 PolicyTokenizer_h___ +#define PolicyTokenizer_h___ + +#include "nsContentUtils.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' ], + * ] + */ + +using policyTokens = nsTArray<CopyableTArray<nsString>>; + +class PolicyTokenizer { + public: + static void tokenizePolicy(const nsAString& aPolicyString, + policyTokens& outTokens); + + private: + PolicyTokenizer(const char16_t* aStart, const char16_t* aEnd); + ~PolicyTokenizer(); + + inline bool atEnd() { return mCurChar >= mEndChar; } + + inline void skipWhiteSpace() { + while (mCurChar < mEndChar && nsContentUtils::IsHTMLWhitespace(*mCurChar)) { + mCurChar++; + } + mCurToken.Truncate(); + } + + inline void skipWhiteSpaceAndSemicolon() { + while (mCurChar < mEndChar && + (*mCurChar == ';' || nsContentUtils::IsHTMLWhitespace(*mCurChar))) { + 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(policyTokens& outTokens); + + const char16_t* mCurChar; + const char16_t* mEndChar; + nsString mCurToken; +}; + +#endif /* PolicyTokenizer_h___ */ diff --git a/dom/security/ReferrerInfo.cpp b/dom/security/ReferrerInfo.cpp new file mode 100644 index 0000000000..0091fe3b95 --- /dev/null +++ b/dom/security/ReferrerInfo.cpp @@ -0,0 +1,1725 @@ +/* -*- 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/dom/ReferrerPolicyBinding.h" +#include "nsIClassInfoImpl.h" +#include "nsIEffectiveTLDService.h" +#include "nsIHttpChannel.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsIOService.h" +#include "nsIPipe.h" +#include "nsIURL.h" + +#include "nsWhitespaceTokenizer.h" +#include "nsAlgorithm.h" +#include "nsContentUtils.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsStreamUtils.h" +#include "ReferrerInfo.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/ContentBlockingAllowList.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/net/HttpBaseChannel.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StorageAccess.h" +#include "mozilla/StyleSheet.h" +#include "mozilla/Telemetry.h" +#include "nsIWebProgressListener.h" + +static mozilla::LazyLogModule gReferrerInfoLog("ReferrerInfo"); +#define LOG(msg) MOZ_LOG(gReferrerInfoLog, mozilla::LogLevel::Debug, msg) +#define LOG_ENABLED() MOZ_LOG_TEST(gReferrerInfoLog, mozilla::LogLevel::Debug) + +using namespace mozilla::net; + +namespace mozilla::dom { + +// Implementation of ClassInfo is required to serialize/deserialize +NS_IMPL_CLASSINFO(ReferrerInfo, nullptr, nsIClassInfo::THREADSAFE, + REFERRERINFO_CID) + +NS_IMPL_ISUPPORTS_CI(ReferrerInfo, nsIReferrerInfo, nsISerializable) + +#define MAX_REFERRER_SENDING_POLICY 2 +#define MAX_CROSS_ORIGIN_SENDING_POLICY 2 +#define MAX_TRIMMING_POLICY 2 + +#define MIN_REFERRER_SENDING_POLICY 0 +#define MIN_CROSS_ORIGIN_SENDING_POLICY 0 +#define MIN_TRIMMING_POLICY 0 + +/* + * Default referrer policy to use + */ +enum DefaultReferrerPolicy : uint32_t { + eDefaultPolicyNoReferrer = 0, + eDefaultPolicySameOrgin = 1, + eDefaultPolicyStrictWhenXorigin = 2, + eDefaultPolicyNoReferrerWhenDownGrade = 3, +}; + +static uint32_t GetDefaultFirstPartyReferrerPolicyPref(bool aPrivateBrowsing) { + return aPrivateBrowsing + ? StaticPrefs::network_http_referer_defaultPolicy_pbmode() + : StaticPrefs::network_http_referer_defaultPolicy(); +} + +static uint32_t GetDefaultThirdPartyReferrerPolicyPref(bool aPrivateBrowsing) { + return aPrivateBrowsing + ? StaticPrefs::network_http_referer_defaultPolicy_trackers_pbmode() + : StaticPrefs::network_http_referer_defaultPolicy_trackers(); +} + +static ReferrerPolicy DefaultReferrerPolicyToReferrerPolicy( + uint32_t aDefaultToUse) { + switch (aDefaultToUse) { + case DefaultReferrerPolicy::eDefaultPolicyNoReferrer: + return ReferrerPolicy::No_referrer; + case DefaultReferrerPolicy::eDefaultPolicySameOrgin: + return ReferrerPolicy::Same_origin; + case DefaultReferrerPolicy::eDefaultPolicyStrictWhenXorigin: + return ReferrerPolicy::Strict_origin_when_cross_origin; + } + + return ReferrerPolicy::No_referrer_when_downgrade; +} + +struct LegacyReferrerPolicyTokenMap { + const char* mToken; + ReferrerPolicy mPolicy; +}; + +/* + * Parse ReferrerPolicy from token. + * The supported tokens are defined in ReferrerPolicy.webidl. + * The legacy tokens are "never", "default", "always" and + * "origin-when-crossorigin". The legacy tokens are only supported in meta + * referrer content + * + * @param aContent content string to be transformed into + * ReferrerPolicyEnum, e.g. "origin". + */ +ReferrerPolicy ReferrerPolicyFromToken(const nsAString& aContent, + bool allowedLegacyToken) { + nsString lowerContent(aContent); + ToLowerCase(lowerContent); + + if (allowedLegacyToken) { + static const LegacyReferrerPolicyTokenMap sLegacyReferrerPolicyToken[] = { + {"never", ReferrerPolicy::No_referrer}, + {"default", ReferrerPolicy::No_referrer_when_downgrade}, + {"always", ReferrerPolicy::Unsafe_url}, + {"origin-when-crossorigin", ReferrerPolicy::Origin_when_cross_origin}, + }; + + uint8_t numStr = (sizeof(sLegacyReferrerPolicyToken) / + sizeof(sLegacyReferrerPolicyToken[0])); + for (uint8_t i = 0; i < numStr; i++) { + if (lowerContent.EqualsASCII(sLegacyReferrerPolicyToken[i].mToken)) { + return sLegacyReferrerPolicyToken[i].mPolicy; + } + } + } + + // Supported tokes - ReferrerPolicyValues, are generated from + // ReferrerPolicy.webidl + for (uint8_t i = 0; ReferrerPolicyValues::strings[i].value; i++) { + if (lowerContent.EqualsASCII(ReferrerPolicyValues::strings[i].value)) { + return static_cast<enum ReferrerPolicy>(i); + } + } + + // Return no referrer policy (empty string) if none of the previous match + return ReferrerPolicy::_empty; +} + +// static +ReferrerPolicy ReferrerInfo::ReferrerPolicyFromMetaString( + const nsAString& aContent) { + // This is implemented as described in + // https://html.spec.whatwg.org/multipage/semantics.html#meta-referrer + // Meta referrer accepts both supported tokens in ReferrerPolicy.webidl and + // legacy tokens. + return ReferrerPolicyFromToken(aContent, true); +} + +// static +ReferrerPolicy ReferrerInfo::ReferrerPolicyAttributeFromString( + const nsAString& aContent) { + // This is implemented as described in + // https://html.spec.whatwg.org/multipage/infrastructure.html#referrer-policy-attribute + // referrerpolicy attribute only accepts supported tokens in + // ReferrerPolicy.webidl + return ReferrerPolicyFromToken(aContent, false); +} + +// static +ReferrerPolicy ReferrerInfo::ReferrerPolicyFromHeaderString( + const nsAString& aContent) { + // Multiple headers could be concatenated into one comma-separated + // list of policies. Need to tokenize the multiple headers. + ReferrerPolicyEnum referrerPolicy = ReferrerPolicy::_empty; + for (const auto& token : nsCharSeparatedTokenizer(aContent, ',').ToRange()) { + if (token.IsEmpty()) { + continue; + } + + // Referrer-Policy header only accepts supported tokens in + // ReferrerPolicy.webidl + ReferrerPolicyEnum policy = ReferrerPolicyFromToken(token, false); + // If there are multiple policies available, the last valid policy should be + // used. + // https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values + if (policy != ReferrerPolicy::_empty) { + referrerPolicy = policy; + } + } + return referrerPolicy; +} + +// static +const char* ReferrerInfo::ReferrerPolicyToString(ReferrerPolicyEnum aPolicy) { + uint8_t index = static_cast<uint8_t>(aPolicy); + uint8_t referrerPolicyCount = ArrayLength(ReferrerPolicyValues::strings); + MOZ_ASSERT(index < referrerPolicyCount); + if (index >= referrerPolicyCount) { + return ""; + } + + return ReferrerPolicyValues::strings[index].value; +} + +/* static */ +uint32_t ReferrerInfo::GetUserReferrerSendingPolicy() { + return clamped<uint32_t>( + StaticPrefs::network_http_sendRefererHeader_DoNotUseDirectly(), + MIN_REFERRER_SENDING_POLICY, MAX_REFERRER_SENDING_POLICY); +} + +/* static */ +uint32_t ReferrerInfo::GetUserXOriginSendingPolicy() { + return clamped<uint32_t>( + StaticPrefs::network_http_referer_XOriginPolicy_DoNotUseDirectly(), + MIN_CROSS_ORIGIN_SENDING_POLICY, MAX_CROSS_ORIGIN_SENDING_POLICY); +} + +/* static */ +uint32_t ReferrerInfo::GetUserTrimmingPolicy() { + return clamped<uint32_t>( + StaticPrefs::network_http_referer_trimmingPolicy_DoNotUseDirectly(), + MIN_TRIMMING_POLICY, MAX_TRIMMING_POLICY); +} + +/* static */ +uint32_t ReferrerInfo::GetUserXOriginTrimmingPolicy() { + return clamped<uint32_t>( + StaticPrefs:: + network_http_referer_XOriginTrimmingPolicy_DoNotUseDirectly(), + MIN_TRIMMING_POLICY, MAX_TRIMMING_POLICY); +} + +/* static */ +ReferrerPolicy ReferrerInfo::GetDefaultReferrerPolicy(nsIHttpChannel* aChannel, + nsIURI* aURI, + bool aPrivateBrowsing) { + bool thirdPartyTrackerIsolated = false; + if (aChannel && aURI) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + nsCOMPtr<nsICookieJarSettings> cjs; + Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cjs)); + if (!cjs) { + bool shouldResistFingerprinting = + nsContentUtils::ShouldResistFingerprinting(aChannel); + cjs = aPrivateBrowsing + ? net::CookieJarSettings::Create(CookieJarSettings::ePrivate, + shouldResistFingerprinting) + : net::CookieJarSettings::Create(CookieJarSettings::eRegular, + shouldResistFingerprinting); + } + + // We only check if the channel is isolated if it's in the parent process + // with the rejection of third party contexts is enabled. We don't need to + // check this in content processes since the tracking state of the channel + // is unknown here and the referrer policy would be updated when the channel + // starts connecting in the parent process. + if (XRE_IsParentProcess() && cjs->GetRejectThirdPartyContexts()) { + uint32_t rejectedReason = 0; + thirdPartyTrackerIsolated = + !ShouldAllowAccessFor(aChannel, aURI, &rejectedReason) && + rejectedReason != + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN); + // Here we intentionally do not notify about the rejection reason, if any + // in order to avoid this check to have any visible side-effects (e.g. a + // web console report.) + } + } + + // Select the appropriate pref starting with + // "network.http.referer.defaultPolicy" to use based on private-browsing + // ("pbmode") AND third-party trackers ("trackers"). + return DefaultReferrerPolicyToReferrerPolicy( + thirdPartyTrackerIsolated + ? GetDefaultThirdPartyReferrerPolicyPref(aPrivateBrowsing) + : GetDefaultFirstPartyReferrerPolicyPref(aPrivateBrowsing)); +} + +/* static */ +bool ReferrerInfo::IsReferrerSchemeAllowed(nsIURI* aReferrer) { + NS_ENSURE_TRUE(aReferrer, false); + + nsAutoCString scheme; + nsresult rv = aReferrer->GetScheme(scheme); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return scheme.EqualsIgnoreCase("https") || scheme.EqualsIgnoreCase("http"); +} + +/* static */ +bool ReferrerInfo::ShouldResponseInheritReferrerInfo(nsIChannel* aChannel) { + if (!aChannel) { + return false; + } + + nsCOMPtr<nsIURI> channelURI; + nsresult rv = aChannel->GetURI(getter_AddRefs(channelURI)); + NS_ENSURE_SUCCESS(rv, false); + + bool isAbout = channelURI->SchemeIs("about"); + if (!isAbout) { + return false; + } + + nsAutoCString aboutSpec; + rv = channelURI->GetSpec(aboutSpec); + NS_ENSURE_SUCCESS(rv, false); + + return aboutSpec.EqualsLiteral("about:srcdoc"); +} + +/* static */ +nsresult ReferrerInfo::HandleSecureToInsecureReferral( + nsIURI* aOriginalURI, nsIURI* aURI, ReferrerPolicyEnum aPolicy, + bool& aAllowed) { + NS_ENSURE_ARG(aOriginalURI); + NS_ENSURE_ARG(aURI); + + aAllowed = false; + + bool referrerIsHttpsScheme = aOriginalURI->SchemeIs("https"); + if (!referrerIsHttpsScheme) { + aAllowed = true; + return NS_OK; + } + + // It's ok to send referrer for https-to-http scenarios if the referrer + // policy is "unsafe-url", "origin", or "origin-when-cross-origin". + // in other referrer policies, https->http is not allowed... + bool uriIsHttpsScheme = aURI->SchemeIs("https"); + if (aPolicy != ReferrerPolicy::Unsafe_url && + aPolicy != ReferrerPolicy::Origin_when_cross_origin && + aPolicy != ReferrerPolicy::Origin && !uriIsHttpsScheme) { + return NS_OK; + } + + aAllowed = true; + return NS_OK; +} + +nsresult ReferrerInfo::HandleUserXOriginSendingPolicy(nsIURI* aURI, + nsIURI* aReferrer, + bool& aAllowed) const { + NS_ENSURE_ARG(aURI); + aAllowed = false; + + nsAutoCString uriHost; + nsAutoCString referrerHost; + + nsresult rv = aURI->GetAsciiHost(uriHost); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aReferrer->GetAsciiHost(referrerHost); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Send an empty referrer if xorigin and leaving a .onion domain. + if (StaticPrefs::network_http_referer_hideOnionSource() && + !uriHost.Equals(referrerHost) && + StringEndsWith(referrerHost, ".onion"_ns)) { + return NS_OK; + } + + switch (GetUserXOriginSendingPolicy()) { + // Check policy for sending referrer only when hosts match + case XOriginSendingPolicy::ePolicySendWhenSameHost: { + if (!uriHost.Equals(referrerHost)) { + return NS_OK; + } + break; + } + + case XOriginSendingPolicy::ePolicySendWhenSameDomain: { + nsCOMPtr<nsIEffectiveTLDService> eTLDService = + do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); + if (!eTLDService) { + // check policy for sending only when effective top level domain + // matches. this falls back on using host if eTLDService does not work + if (!uriHost.Equals(referrerHost)) { + return NS_OK; + } + break; + } + + nsAutoCString uriDomain; + nsAutoCString referrerDomain; + uint32_t extraDomains = 0; + + rv = eTLDService->GetBaseDomain(aURI, extraDomains, uriDomain); + if (rv == NS_ERROR_HOST_IS_IP_ADDRESS || + rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) { + // uri is either an IP address, an alias such as 'localhost', an eTLD + // such as 'co.uk', or the empty string. Uses the normalized host in + // such cases. + rv = aURI->GetAsciiHost(uriDomain); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = eTLDService->GetBaseDomain(aReferrer, extraDomains, referrerDomain); + if (rv == NS_ERROR_HOST_IS_IP_ADDRESS || + rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) { + // referrer is either an IP address, an alias such as 'localhost', an + // eTLD such as 'co.uk', or the empty string. Uses the normalized host + // in such cases. + rv = aReferrer->GetAsciiHost(referrerDomain); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!uriDomain.Equals(referrerDomain)) { + return NS_OK; + } + break; + } + + default: + break; + } + + aAllowed = true; + return NS_OK; +} + +// This roughly implements Step 3.1. of +// https://fetch.spec.whatwg.org/#append-a-request-origin-header +/* static */ +bool ReferrerInfo::ShouldSetNullOriginHeader(net::HttpBaseChannel* aChannel, + nsIURI* aOriginURI) { + MOZ_ASSERT(aChannel); + MOZ_ASSERT(aOriginURI); + + // If request’s mode is not "cors", then switch on request’s referrer policy: + RequestMode requestMode = RequestMode::No_cors; + MOZ_ALWAYS_SUCCEEDS(aChannel->GetRequestMode(&requestMode)); + if (requestMode == RequestMode::Cors) { + return false; + } + + nsCOMPtr<nsIReferrerInfo> referrerInfo; + NS_ENSURE_SUCCESS(aChannel->GetReferrerInfo(getter_AddRefs(referrerInfo)), + false); + if (!referrerInfo) { + return false; + } + + // "no-referrer": + enum ReferrerPolicy policy = referrerInfo->ReferrerPolicy(); + if (policy == ReferrerPolicy::No_referrer) { + // Set serializedOrigin to `null`. + // Note: Returning true is the same as setting the serializedOrigin to null + // in this method. + return true; + } + + // "no-referrer-when-downgrade": + // "strict-origin": + // "strict-origin-when-cross-origin": + // If request’s origin is a tuple origin, its scheme is "https", and + // request’s current URL’s scheme is not "https", then set serializedOrigin + // to `null`. + bool allowed = false; + nsCOMPtr<nsIURI> uri; + NS_ENSURE_SUCCESS(aChannel->GetURI(getter_AddRefs(uri)), false); + if (NS_SUCCEEDED(ReferrerInfo::HandleSecureToInsecureReferral( + aOriginURI, uri, policy, allowed)) && + !allowed) { + return true; + } + + // "same-origin": + if (policy == ReferrerPolicy::Same_origin) { + // If request’s origin is not same origin with request’s current URL’s + // origin, then set serializedOrigin to `null`. + return ReferrerInfo::IsCrossOriginRequest(aChannel); + } + + // Otherwise: + // Do Nothing. + return false; +} + +nsresult ReferrerInfo::HandleUserReferrerSendingPolicy(nsIHttpChannel* aChannel, + bool& aAllowed) const { + aAllowed = false; + uint32_t referrerSendingPolicy; + uint32_t loadFlags; + nsresult rv = aChannel->GetLoadFlags(&loadFlags); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (loadFlags & nsIHttpChannel::LOAD_INITIAL_DOCUMENT_URI) { + referrerSendingPolicy = ReferrerSendingPolicy::ePolicySendWhenUserTrigger; + } else { + referrerSendingPolicy = ReferrerSendingPolicy::ePolicySendInlineContent; + } + if (GetUserReferrerSendingPolicy() < referrerSendingPolicy) { + return NS_OK; + } + + aAllowed = true; + return NS_OK; +} + +/* static */ +bool ReferrerInfo::IsCrossOriginRequest(nsIHttpChannel* aChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + if (!loadInfo->TriggeringPrincipal()->GetIsContentPrincipal()) { + LOG(("no triggering URI via loadInfo, assuming load is cross-origin")); + return true; + } + + if (LOG_ENABLED()) { + nsAutoCString triggeringURISpec; + loadInfo->TriggeringPrincipal()->GetAsciiSpec(triggeringURISpec); + LOG(("triggeringURI=%s\n", triggeringURISpec.get())); + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return true; + } + + return !loadInfo->TriggeringPrincipal()->IsSameOrigin(uri); +} + +/* static */ +bool ReferrerInfo::IsCrossSiteRequest(nsIHttpChannel* aChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + if (!loadInfo->TriggeringPrincipal()->GetIsContentPrincipal()) { + LOG(("no triggering URI via loadInfo, assuming load is cross-site")); + return true; + } + + if (LOG_ENABLED()) { + nsAutoCString triggeringURISpec; + loadInfo->TriggeringPrincipal()->GetAsciiSpec(triggeringURISpec); + LOG(("triggeringURI=%s\n", triggeringURISpec.get())); + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return true; + } + + bool isCrossSite = true; + rv = loadInfo->TriggeringPrincipal()->IsThirdPartyURI(uri, &isCrossSite); + if (NS_FAILED(rv)) { + return true; + } + + return isCrossSite; +} + +ReferrerInfo::TrimmingPolicy ReferrerInfo::ComputeTrimmingPolicy( + nsIHttpChannel* aChannel) const { + uint32_t trimmingPolicy = GetUserTrimmingPolicy(); + + switch (mPolicy) { + case ReferrerPolicy::Origin: + case ReferrerPolicy::Strict_origin: + trimmingPolicy = TrimmingPolicy::ePolicySchemeHostPort; + break; + + case ReferrerPolicy::Origin_when_cross_origin: + case ReferrerPolicy::Strict_origin_when_cross_origin: + if (trimmingPolicy != TrimmingPolicy::ePolicySchemeHostPort && + IsCrossOriginRequest(aChannel)) { + // Ignore set trimmingPolicy if it is already the strictest + // policy. + trimmingPolicy = TrimmingPolicy::ePolicySchemeHostPort; + } + break; + + // This function is called when a nonempty referrer value is allowed to + // send. For the next 3 policies: same-origin, no-referrer-when-downgrade, + // unsafe-url, without trimming we should have a full uri. And the trimming + // policy only depends on user prefs. + case ReferrerPolicy::Same_origin: + case ReferrerPolicy::No_referrer_when_downgrade: + case ReferrerPolicy::Unsafe_url: + if (trimmingPolicy != TrimmingPolicy::ePolicySchemeHostPort) { + // Ignore set trimmingPolicy if it is already the strictest + // policy. Apply the user cross-origin trimming policy if it's more + // restrictive than the general one. + if (GetUserXOriginTrimmingPolicy() != TrimmingPolicy::ePolicyFullURI && + IsCrossOriginRequest(aChannel)) { + trimmingPolicy = + std::max(trimmingPolicy, GetUserXOriginTrimmingPolicy()); + } + } + break; + + case ReferrerPolicy::No_referrer: + case ReferrerPolicy::_empty: + default: + MOZ_ASSERT_UNREACHABLE("Unexpected value"); + break; + } + + return static_cast<TrimmingPolicy>(trimmingPolicy); +} + +nsresult ReferrerInfo::LimitReferrerLength( + nsIHttpChannel* aChannel, nsIURI* aReferrer, TrimmingPolicy aTrimmingPolicy, + nsACString& aInAndOutTrimmedReferrer) const { + if (!StaticPrefs::network_http_referer_referrerLengthLimit()) { + return NS_OK; + } + + if (aInAndOutTrimmedReferrer.Length() <= + StaticPrefs::network_http_referer_referrerLengthLimit()) { + return NS_OK; + } + + nsAutoString referrerLengthLimit; + referrerLengthLimit.AppendInt( + StaticPrefs::network_http_referer_referrerLengthLimit()); + if (aTrimmingPolicy == ePolicyFullURI || + aTrimmingPolicy == ePolicySchemeHostPortPath) { + // If referrer header is over max Length, down to origin + nsresult rv = GetOriginFromReferrerURI(aReferrer, aInAndOutTrimmedReferrer); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Step 6 within https://w3c.github.io/webappsec-referrer-policy/#strip-url + // states that the trailing "/" does not need to get stripped. However, + // GetOriginFromReferrerURI() also removes any trailing "/" hence we have to + // add it back here. + aInAndOutTrimmedReferrer.AppendLiteral("/"); + if (aInAndOutTrimmedReferrer.Length() <= + StaticPrefs::network_http_referer_referrerLengthLimit()) { + AutoTArray<nsString, 2> params = { + referrerLengthLimit, NS_ConvertUTF8toUTF16(aInAndOutTrimmedReferrer)}; + LogMessageToConsole(aChannel, "ReferrerLengthOverLimitation", params); + return NS_OK; + } + } + + // If we end up here either the trimmingPolicy is equal to + // 'ePolicySchemeHostPort' or the 'origin' of any other policy is still over + // the length limit. If so, truncate the referrer entirely. + AutoTArray<nsString, 2> params = { + referrerLengthLimit, NS_ConvertUTF8toUTF16(aInAndOutTrimmedReferrer)}; + LogMessageToConsole(aChannel, "ReferrerOriginLengthOverLimitation", params); + aInAndOutTrimmedReferrer.Truncate(); + + return NS_OK; +} + +nsresult ReferrerInfo::GetOriginFromReferrerURI(nsIURI* aReferrer, + nsACString& aResult) const { + MOZ_ASSERT(aReferrer); + aResult.Truncate(); + // We want the IDN-normalized PrePath. That's not something currently + // available and there doesn't yet seem to be justification for adding it to + // the interfaces, so just build it up from scheme+AsciiHostPort + nsAutoCString scheme, asciiHostPort; + nsresult rv = aReferrer->GetScheme(scheme); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aResult = scheme; + aResult.AppendLiteral("://"); + // Note we explicitly cleared UserPass above, so do not need to build it. + rv = aReferrer->GetAsciiHostPort(asciiHostPort); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aResult.Append(asciiHostPort); + return NS_OK; +} + +nsresult ReferrerInfo::TrimReferrerWithPolicy(nsIURI* aReferrer, + TrimmingPolicy aTrimmingPolicy, + nsACString& aResult) const { + MOZ_ASSERT(aReferrer); + + if (aTrimmingPolicy == TrimmingPolicy::ePolicyFullURI) { + return aReferrer->GetAsciiSpec(aResult); + } + + nsresult rv = GetOriginFromReferrerURI(aReferrer, aResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aTrimmingPolicy == TrimmingPolicy::ePolicySchemeHostPortPath) { + nsCOMPtr<nsIURL> url(do_QueryInterface(aReferrer)); + if (url) { + nsAutoCString path; + rv = url->GetFilePath(path); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aResult.Append(path); + return NS_OK; + } + } + + // Step 6 within https://w3c.github.io/webappsec-referrer-policy/#strip-url + // states that the trailing "/" does not need to get stripped. However, + // GetOriginFromReferrerURI() also removes any trailing "/" hence we have to + // add it back here. + aResult.AppendLiteral("/"); + return NS_OK; +} + +bool ReferrerInfo::ShouldIgnoreLessRestrictedPolicies( + nsIHttpChannel* aChannel, const ReferrerPolicyEnum aPolicy) const { + MOZ_ASSERT(aChannel); + + // We only care about the less restricted policies. + if (aPolicy != ReferrerPolicy::Unsafe_url && + aPolicy != ReferrerPolicy::No_referrer_when_downgrade && + aPolicy != ReferrerPolicy::Origin_when_cross_origin) { + return false; + } + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + bool isPrivate = NS_UsePrivateBrowsing(aChannel); + + // Return early if we don't want to ignore less restricted policies for the + // top navigation. + if (loadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_DOCUMENT) { + bool isEnabledForTopNavigation = + isPrivate + ? StaticPrefs:: + network_http_referer_disallowCrossSiteRelaxingDefault_pbmode_top_navigation() + : StaticPrefs:: + network_http_referer_disallowCrossSiteRelaxingDefault_top_navigation(); + if (!isEnabledForTopNavigation) { + return false; + } + + // We have to get the value of the contentBlockingAllowList earlier because + // the channel hasn't been opened yet here. Note that we only need to do + // this for first-party navigation. For third-party loads, the value is + // inherited from the parent. + if (XRE_IsParentProcess()) { + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + Unused << loadInfo->GetCookieJarSettings( + getter_AddRefs(cookieJarSettings)); + + net::CookieJarSettings::Cast(cookieJarSettings) + ->UpdateIsOnContentBlockingAllowList(aChannel); + } + } + + // We don't ignore less restricted referrer policies if ETP is toggled off. + // This would affect iframe loads and top navigation. For iframes, it will + // stop ignoring if the first-party site toggled ETP off. For top navigation, + // it depends on the ETP toggle for the destination site. + if (ContentBlockingAllowList::Check(aChannel)) { + return false; + } + + bool isCrossSite = IsCrossSiteRequest(aChannel); + bool isEnabled = + isPrivate + ? StaticPrefs:: + network_http_referer_disallowCrossSiteRelaxingDefault_pbmode() + : StaticPrefs:: + network_http_referer_disallowCrossSiteRelaxingDefault(); + + if (!isEnabled) { + // Log the warning message to console to inform that we will ignore + // less restricted policies for cross-site requests in the future. + if (isCrossSite) { + nsCOMPtr<nsIURI> uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, false); + + AutoTArray<nsString, 1> params = { + NS_ConvertUTF8toUTF16(uri->GetSpecOrDefault())}; + LogMessageToConsole(aChannel, "ReferrerPolicyDisallowRelaxingWarning", + params); + } + return false; + } + + // Check if the channel is triggered by the system or the extension. + auto* triggerBasePrincipal = + BasePrincipal::Cast(loadInfo->TriggeringPrincipal()); + if (triggerBasePrincipal->IsSystemPrincipal() || + triggerBasePrincipal->AddonPolicy()) { + return false; + } + + if (isCrossSite) { + // Log the console message to say that the less restricted policy was + // ignored. + nsCOMPtr<nsIURI> uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, true); + + uint32_t idx = static_cast<uint32_t>(aPolicy); + + AutoTArray<nsString, 2> params = { + NS_ConvertUTF8toUTF16( + nsDependentCString(ReferrerPolicyValues::strings[idx].value)), + NS_ConvertUTF8toUTF16(uri->GetSpecOrDefault())}; + LogMessageToConsole(aChannel, "ReferrerPolicyDisallowRelaxingMessage", + params); + } + + return isCrossSite; +} + +void ReferrerInfo::LogMessageToConsole( + nsIHttpChannel* aChannel, const char* aMsg, + const nsTArray<nsString>& aParams) const { + MOZ_ASSERT(aChannel); + + nsCOMPtr<nsIURI> uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + uint64_t windowID = 0; + + rv = aChannel->GetTopLevelContentWindowId(&windowID); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + if (!windowID) { + nsCOMPtr<nsILoadGroup> loadGroup; + rv = aChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + if (loadGroup) { + windowID = nsContentUtils::GetInnerWindowID(loadGroup); + } + } + + nsAutoString localizedMsg; + rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eSECURITY_PROPERTIES, aMsg, aParams, localizedMsg); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + rv = nsContentUtils::ReportToConsoleByWindowID( + localizedMsg, nsIScriptError::infoFlag, "Security"_ns, windowID, uri); + Unused << NS_WARN_IF(NS_FAILED(rv)); +} + +ReferrerPolicy ReferrerPolicyIDLToReferrerPolicy( + nsIReferrerInfo::ReferrerPolicyIDL aReferrerPolicy) { + switch (aReferrerPolicy) { + case nsIReferrerInfo::EMPTY: + return ReferrerPolicy::_empty; + break; + case nsIReferrerInfo::NO_REFERRER: + return ReferrerPolicy::No_referrer; + break; + case nsIReferrerInfo::NO_REFERRER_WHEN_DOWNGRADE: + return ReferrerPolicy::No_referrer_when_downgrade; + break; + case nsIReferrerInfo::ORIGIN: + return ReferrerPolicy::Origin; + break; + case nsIReferrerInfo::ORIGIN_WHEN_CROSS_ORIGIN: + return ReferrerPolicy::Origin_when_cross_origin; + break; + case nsIReferrerInfo::UNSAFE_URL: + return ReferrerPolicy::Unsafe_url; + break; + case nsIReferrerInfo::SAME_ORIGIN: + return ReferrerPolicy::Same_origin; + break; + case nsIReferrerInfo::STRICT_ORIGIN: + return ReferrerPolicy::Strict_origin; + break; + case nsIReferrerInfo::STRICT_ORIGIN_WHEN_CROSS_ORIGIN: + return ReferrerPolicy::Strict_origin_when_cross_origin; + break; + default: + MOZ_ASSERT_UNREACHABLE("Invalid ReferrerPolicy value"); + break; + } + + return ReferrerPolicy::_empty; +} + +nsIReferrerInfo::ReferrerPolicyIDL ReferrerPolicyToReferrerPolicyIDL( + ReferrerPolicy aReferrerPolicy) { + switch (aReferrerPolicy) { + case ReferrerPolicy::_empty: + return nsIReferrerInfo::EMPTY; + break; + case ReferrerPolicy::No_referrer: + return nsIReferrerInfo::NO_REFERRER; + break; + case ReferrerPolicy::No_referrer_when_downgrade: + return nsIReferrerInfo::NO_REFERRER_WHEN_DOWNGRADE; + break; + case ReferrerPolicy::Origin: + return nsIReferrerInfo::ORIGIN; + break; + case ReferrerPolicy::Origin_when_cross_origin: + return nsIReferrerInfo::ORIGIN_WHEN_CROSS_ORIGIN; + break; + case ReferrerPolicy::Unsafe_url: + return nsIReferrerInfo::UNSAFE_URL; + break; + case ReferrerPolicy::Same_origin: + return nsIReferrerInfo::SAME_ORIGIN; + break; + case ReferrerPolicy::Strict_origin: + return nsIReferrerInfo::STRICT_ORIGIN; + break; + case ReferrerPolicy::Strict_origin_when_cross_origin: + return nsIReferrerInfo::STRICT_ORIGIN_WHEN_CROSS_ORIGIN; + break; + default: + MOZ_ASSERT_UNREACHABLE("Invalid ReferrerPolicy value"); + break; + } + + return nsIReferrerInfo::EMPTY; +} + +ReferrerInfo::ReferrerInfo() + : mOriginalReferrer(nullptr), + mPolicy(ReferrerPolicy::_empty), + mOriginalPolicy(ReferrerPolicy::_empty), + mSendReferrer(true), + mInitialized(false), + mOverridePolicyByDefault(false) {} + +ReferrerInfo::ReferrerInfo(const Document& aDoc) : ReferrerInfo() { + InitWithDocument(&aDoc); +} + +ReferrerInfo::ReferrerInfo(const Element& aElement) : ReferrerInfo() { + InitWithElement(&aElement); +} + +ReferrerInfo::ReferrerInfo(nsIURI* aOriginalReferrer, + ReferrerPolicyEnum aPolicy, bool aSendReferrer, + const Maybe<nsCString>& aComputedReferrer) + : mOriginalReferrer(aOriginalReferrer), + mPolicy(aPolicy), + mOriginalPolicy(aPolicy), + mSendReferrer(aSendReferrer), + mInitialized(true), + mOverridePolicyByDefault(false), + mComputedReferrer(aComputedReferrer) {} + +ReferrerInfo::ReferrerInfo(const ReferrerInfo& rhs) + : mOriginalReferrer(rhs.mOriginalReferrer), + mPolicy(rhs.mPolicy), + mOriginalPolicy(rhs.mOriginalPolicy), + mSendReferrer(rhs.mSendReferrer), + mInitialized(rhs.mInitialized), + mOverridePolicyByDefault(rhs.mOverridePolicyByDefault), + mComputedReferrer(rhs.mComputedReferrer) {} + +already_AddRefed<ReferrerInfo> ReferrerInfo::Clone() const { + RefPtr<ReferrerInfo> copy(new ReferrerInfo(*this)); + return copy.forget(); +} + +already_AddRefed<ReferrerInfo> ReferrerInfo::CloneWithNewPolicy( + ReferrerPolicyEnum aPolicy) const { + RefPtr<ReferrerInfo> copy(new ReferrerInfo(*this)); + copy->mPolicy = aPolicy; + copy->mOriginalPolicy = aPolicy; + return copy.forget(); +} + +already_AddRefed<ReferrerInfo> ReferrerInfo::CloneWithNewSendReferrer( + bool aSendReferrer) const { + RefPtr<ReferrerInfo> copy(new ReferrerInfo(*this)); + copy->mSendReferrer = aSendReferrer; + return copy.forget(); +} + +already_AddRefed<ReferrerInfo> ReferrerInfo::CloneWithNewOriginalReferrer( + nsIURI* aOriginalReferrer) const { + RefPtr<ReferrerInfo> copy(new ReferrerInfo(*this)); + copy->mOriginalReferrer = aOriginalReferrer; + return copy.forget(); +} + +NS_IMETHODIMP +ReferrerInfo::GetOriginalReferrer(nsIURI** aOriginalReferrer) { + *aOriginalReferrer = mOriginalReferrer; + NS_IF_ADDREF(*aOriginalReferrer); + return NS_OK; +} + +NS_IMETHODIMP +ReferrerInfo::GetReferrerPolicy( + JSContext* aCx, nsIReferrerInfo::ReferrerPolicyIDL* aReferrerPolicy) { + *aReferrerPolicy = ReferrerPolicyToReferrerPolicyIDL(mPolicy); + return NS_OK; +} + +NS_IMETHODIMP +ReferrerInfo::GetReferrerPolicyString(nsACString& aResult) { + aResult.AssignASCII(ReferrerPolicyToString(mPolicy)); + return NS_OK; +} + +ReferrerPolicy ReferrerInfo::ReferrerPolicy() { return mPolicy; } + +NS_IMETHODIMP +ReferrerInfo::GetSendReferrer(bool* aSendReferrer) { + *aSendReferrer = mSendReferrer; + return NS_OK; +} + +NS_IMETHODIMP +ReferrerInfo::Equals(nsIReferrerInfo* aOther, bool* aResult) { + NS_ENSURE_TRUE(aOther, NS_ERROR_INVALID_ARG); + MOZ_ASSERT(mInitialized); + if (aOther == this) { + *aResult = true; + return NS_OK; + } + + *aResult = false; + ReferrerInfo* other = static_cast<ReferrerInfo*>(aOther); + MOZ_ASSERT(other->mInitialized); + + if (mPolicy != other->mPolicy || mSendReferrer != other->mSendReferrer || + mOverridePolicyByDefault != other->mOverridePolicyByDefault || + mComputedReferrer != other->mComputedReferrer) { + return NS_OK; + } + + if (!mOriginalReferrer != !other->mOriginalReferrer) { + // One or the other has mOriginalReferrer, but not both... not equal + return NS_OK; + } + + bool originalReferrerEquals; + if (mOriginalReferrer && + (NS_FAILED(mOriginalReferrer->Equals(other->mOriginalReferrer, + &originalReferrerEquals)) || + !originalReferrerEquals)) { + return NS_OK; + } + + *aResult = true; + return NS_OK; +} + +NS_IMETHODIMP +ReferrerInfo::GetComputedReferrerSpec(nsAString& aComputedReferrerSpec) { + aComputedReferrerSpec.Assign( + mComputedReferrer.isSome() + ? NS_ConvertUTF8toUTF16(mComputedReferrer.value()) + : EmptyString()); + return NS_OK; +} + +already_AddRefed<nsIURI> ReferrerInfo::GetComputedReferrer() { + if (!mComputedReferrer.isSome() || mComputedReferrer.value().IsEmpty()) { + return nullptr; + } + + nsCOMPtr<nsIURI> result; + nsresult rv = NS_NewURI(getter_AddRefs(result), mComputedReferrer.value()); + if (NS_FAILED(rv)) { + return nullptr; + } + + return result.forget(); +} + +HashNumber ReferrerInfo::Hash() const { + MOZ_ASSERT(mInitialized); + nsAutoCString originalReferrerSpec; + if (mOriginalReferrer) { + Unused << mOriginalReferrer->GetSpec(originalReferrerSpec); + } + + return mozilla::AddToHash( + static_cast<uint32_t>(mPolicy), mSendReferrer, mOverridePolicyByDefault, + mozilla::HashString(originalReferrerSpec), + mozilla::HashString(mComputedReferrer.isSome() ? mComputedReferrer.value() + : ""_ns)); +} + +NS_IMETHODIMP +ReferrerInfo::Init(nsIReferrerInfo::ReferrerPolicyIDL aReferrerPolicy, + bool aSendReferrer, nsIURI* aOriginalReferrer) { + MOZ_ASSERT(!mInitialized); + if (mInitialized) { + return NS_ERROR_ALREADY_INITIALIZED; + }; + + mPolicy = ReferrerPolicyIDLToReferrerPolicy(aReferrerPolicy); + mOriginalPolicy = mPolicy; + mSendReferrer = aSendReferrer; + mOriginalReferrer = aOriginalReferrer; + mInitialized = true; + return NS_OK; +} + +NS_IMETHODIMP +ReferrerInfo::InitWithDocument(const Document* aDocument) { + MOZ_ASSERT(!mInitialized); + if (mInitialized) { + return NS_ERROR_ALREADY_INITIALIZED; + }; + + mPolicy = aDocument->GetReferrerPolicy(); + mOriginalPolicy = mPolicy; + mSendReferrer = true; + mOriginalReferrer = aDocument->GetDocumentURIAsReferrer(); + mInitialized = true; + return NS_OK; +} + +/** + * Check whether the given node has referrerpolicy attribute and parse + * referrer policy from the attribute. + * Currently, referrerpolicy attribute is supported in a, area, img, iframe, + * script, or link element. + */ +static ReferrerPolicy ReferrerPolicyFromAttribute(const Element& aElement) { + if (!aElement.IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area, + nsGkAtoms::script, nsGkAtoms::iframe, + nsGkAtoms::link, nsGkAtoms::img)) { + return ReferrerPolicy::_empty; + } + return aElement.GetReferrerPolicyAsEnum(); +} + +static bool HasRelNoReferrer(const Element& aElement) { + // rel=noreferrer is only support in <a> and <area> + if (!aElement.IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area)) { + return false; + } + + nsAutoString rel; + aElement.GetAttr(nsGkAtoms::rel, rel); + nsWhitespaceTokenizerTemplate<nsContentUtils::IsHTMLWhitespace> tok(rel); + + while (tok.hasMoreTokens()) { + const nsAString& token = tok.nextToken(); + if (token.LowerCaseEqualsLiteral("noreferrer")) { + return true; + } + } + + return false; +} + +NS_IMETHODIMP +ReferrerInfo::InitWithElement(const Element* aElement) { + MOZ_ASSERT(!mInitialized); + if (mInitialized) { + return NS_ERROR_ALREADY_INITIALIZED; + }; + + // Referrer policy from referrerpolicy attribute will have a higher priority + // than referrer policy from <meta> tag and Referrer-Policy header. + mPolicy = ReferrerPolicyFromAttribute(*aElement); + if (mPolicy == ReferrerPolicy::_empty) { + // Fallback to use document's referrer poicy if we don't have referrer + // policy from attribute. + mPolicy = aElement->OwnerDoc()->GetReferrerPolicy(); + } + + mOriginalPolicy = mPolicy; + mSendReferrer = !HasRelNoReferrer(*aElement); + mOriginalReferrer = aElement->OwnerDoc()->GetDocumentURIAsReferrer(); + + mInitialized = true; + return NS_OK; +} + +/* static */ +already_AddRefed<nsIReferrerInfo> +ReferrerInfo::CreateFromOtherAndPolicyOverride( + nsIReferrerInfo* aOther, ReferrerPolicyEnum aPolicyOverride) { + MOZ_ASSERT(aOther); + ReferrerPolicyEnum policy = aPolicyOverride != ReferrerPolicy::_empty + ? aPolicyOverride + : aOther->ReferrerPolicy(); + + nsCOMPtr<nsIURI> referrer = aOther->GetComputedReferrer(); + nsCOMPtr<nsIReferrerInfo> referrerInfo = + new ReferrerInfo(referrer, policy, aOther->GetSendReferrer()); + return referrerInfo.forget(); +} + +/* static */ +already_AddRefed<nsIReferrerInfo> +ReferrerInfo::CreateFromDocumentAndPolicyOverride( + Document* aDoc, ReferrerPolicyEnum aPolicyOverride) { + MOZ_ASSERT(aDoc); + ReferrerPolicyEnum policy = aPolicyOverride != ReferrerPolicy::_empty + ? aPolicyOverride + : aDoc->GetReferrerPolicy(); + nsCOMPtr<nsIReferrerInfo> referrerInfo = + new ReferrerInfo(aDoc->GetDocumentURIAsReferrer(), policy); + return referrerInfo.forget(); +} + +/* static */ +already_AddRefed<nsIReferrerInfo> ReferrerInfo::CreateForFetch( + nsIPrincipal* aPrincipal, Document* aDoc) { + MOZ_ASSERT(aPrincipal); + + nsCOMPtr<nsIReferrerInfo> referrerInfo; + if (!aPrincipal || aPrincipal->IsSystemPrincipal()) { + referrerInfo = new ReferrerInfo(nullptr); + return referrerInfo.forget(); + } + + if (!aDoc) { + aPrincipal->CreateReferrerInfo(ReferrerPolicy::_empty, + getter_AddRefs(referrerInfo)); + return referrerInfo.forget(); + } + + // If it weren't for history.push/replaceState, we could just use the + // principal's URI here. But since we want changes to the URI effected + // by push/replaceState to be reflected in the XHR referrer, we have to + // be more clever. + // + // If the document's original URI (before any push/replaceStates) matches + // our principal, then we use the document's current URI (after + // push/replaceStates). Otherwise (if the document is, say, a data: + // URI), we just use the principal's URI. + nsCOMPtr<nsIURI> docCurURI = aDoc->GetDocumentURI(); + nsCOMPtr<nsIURI> docOrigURI = aDoc->GetOriginalURI(); + + if (docCurURI && docOrigURI) { + bool equal = false; + aPrincipal->EqualsURI(docOrigURI, &equal); + if (equal) { + referrerInfo = new ReferrerInfo(docCurURI, aDoc->GetReferrerPolicy()); + return referrerInfo.forget(); + } + } + aPrincipal->CreateReferrerInfo(aDoc->GetReferrerPolicy(), + getter_AddRefs(referrerInfo)); + return referrerInfo.forget(); +} + +/* static */ +already_AddRefed<nsIReferrerInfo> ReferrerInfo::CreateForExternalCSSResources( + mozilla::StyleSheet* aExternalSheet, ReferrerPolicyEnum aPolicy) { + MOZ_ASSERT(aExternalSheet && !aExternalSheet->IsInline()); + nsCOMPtr<nsIReferrerInfo> referrerInfo; + + // Step 2 + // https://w3c.github.io/webappsec-referrer-policy/#integration-with-css + // Use empty policy at the beginning and update it later from Referrer-Policy + // header. + referrerInfo = new ReferrerInfo(aExternalSheet->GetSheetURI(), aPolicy); + return referrerInfo.forget(); +} + +/* static */ +already_AddRefed<nsIReferrerInfo> ReferrerInfo::CreateForInternalCSSResources( + Document* aDocument) { + MOZ_ASSERT(aDocument); + nsCOMPtr<nsIReferrerInfo> referrerInfo; + + referrerInfo = new ReferrerInfo(aDocument->GetDocumentURI(), + aDocument->GetReferrerPolicy()); + return referrerInfo.forget(); +} + +// Bug 1415044 to investigate which referrer and policy we should use +/* static */ +already_AddRefed<nsIReferrerInfo> ReferrerInfo::CreateForSVGResources( + Document* aDocument) { + MOZ_ASSERT(aDocument); + nsCOMPtr<nsIReferrerInfo> referrerInfo; + + referrerInfo = new ReferrerInfo(aDocument->GetDocumentURI(), + aDocument->GetReferrerPolicy()); + return referrerInfo.forget(); +} + +nsresult ReferrerInfo::ComputeReferrer(nsIHttpChannel* aChannel) { + NS_ENSURE_ARG(aChannel); + MOZ_ASSERT(NS_IsMainThread()); + + // If the referrerInfo is passed around when redirect, just use the last + // computedReferrer to recompute + nsCOMPtr<nsIURI> referrer; + nsresult rv = NS_OK; + mOverridePolicyByDefault = false; + + if (mComputedReferrer.isSome()) { + if (mComputedReferrer.value().IsEmpty()) { + return NS_OK; + } + + rv = NS_NewURI(getter_AddRefs(referrer), mComputedReferrer.value()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + mComputedReferrer.reset(); + // Emplace mComputedReferrer with an empty string, which means we have + // computed the referrer and the result referrer value is empty (not send + // referrer). So any early return later than this line will use that empty + // referrer. + mComputedReferrer.emplace(""_ns); + + if (!mSendReferrer || !mOriginalReferrer || + mPolicy == ReferrerPolicy::No_referrer) { + return NS_OK; + } + + if (mPolicy == ReferrerPolicy::_empty || + ShouldIgnoreLessRestrictedPolicies(aChannel, mOriginalPolicy)) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + OriginAttributes attrs = loadInfo->GetOriginAttributes(); + bool isPrivate = attrs.mPrivateBrowsingId > 0; + + nsCOMPtr<nsIURI> uri; + rv = aChannel->GetURI(getter_AddRefs(uri)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mPolicy = GetDefaultReferrerPolicy(aChannel, uri, isPrivate); + mOverridePolicyByDefault = true; + } + + // This is for the case where the ETP toggle is off. In this case, we need to + // reset the referrer and the policy if the original policy is different from + // the current policy in order to recompute the referrer policy with the + // original policy. + if (!mOverridePolicyByDefault && mOriginalPolicy != ReferrerPolicy::_empty && + mPolicy != mOriginalPolicy) { + referrer = nullptr; + mPolicy = mOriginalPolicy; + } + + if (mPolicy == ReferrerPolicy::No_referrer) { + return NS_OK; + } + + bool isUserReferrerSendingAllowed = false; + rv = HandleUserReferrerSendingPolicy(aChannel, isUserReferrerSendingAllowed); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!isUserReferrerSendingAllowed) { + return NS_OK; + } + + // Enforce Referrer allowlist, only http, https scheme are allowed + if (!IsReferrerSchemeAllowed(mOriginalReferrer)) { + return NS_OK; + } + + nsCOMPtr<nsIURI> uri; + rv = aChannel->GetURI(getter_AddRefs(uri)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool isSecureToInsecureAllowed = false; + rv = HandleSecureToInsecureReferral(mOriginalReferrer, uri, mPolicy, + isSecureToInsecureAllowed); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!isSecureToInsecureAllowed) { + return NS_OK; + } + + // Don't send referrer when the request is cross-origin and policy is + // "same-origin". + if (mPolicy == ReferrerPolicy::Same_origin && + IsCrossOriginRequest(aChannel)) { + return NS_OK; + } + + // Strip away any fragment per RFC 2616 section 14.36 + // and Referrer Policy section 6.3.5. + if (!referrer) { + rv = NS_GetURIWithoutRef(mOriginalReferrer, getter_AddRefs(referrer)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + bool isUserXOriginAllowed = false; + rv = HandleUserXOriginSendingPolicy(uri, referrer, isUserXOriginAllowed); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!isUserXOriginAllowed) { + return NS_OK; + } + + // Handle user pref network.http.referer.spoofSource, send spoofed referrer if + // desired + if (StaticPrefs::network_http_referer_spoofSource()) { + nsCOMPtr<nsIURI> userSpoofReferrer; + rv = NS_GetURIWithoutRef(uri, getter_AddRefs(userSpoofReferrer)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + referrer = userSpoofReferrer; + } + + // strip away any userpass; we don't want to be giving out passwords ;-) + // This is required by Referrer Policy stripping algorithm. + nsCOMPtr<nsIURI> exposableURI = nsIOService::CreateExposableURI(referrer); + referrer = exposableURI; + + TrimmingPolicy trimmingPolicy = ComputeTrimmingPolicy(aChannel); + + nsAutoCString trimmedReferrer; + // We first trim the referrer according to the policy by calling + // 'TrimReferrerWithPolicy' and right after we have to call + // 'LimitReferrerLength' (using the same arguments) because the trimmed + // referrer might exceed the allowed max referrer length. + rv = TrimReferrerWithPolicy(referrer, trimmingPolicy, trimmedReferrer); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = LimitReferrerLength(aChannel, referrer, trimmingPolicy, trimmedReferrer); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // finally, remember the referrer spec. + mComputedReferrer.reset(); + mComputedReferrer.emplace(trimmedReferrer); + + return NS_OK; +} + +/* ===== nsISerializable implementation ====== */ + +nsresult ReferrerInfo::ReadTailDataBeforeGecko100( + const uint32_t& aData, nsIObjectInputStream* aInputStream) { + MOZ_ASSERT(aInputStream); + + nsCOMPtr<nsIInputStream> reader; + nsCOMPtr<nsIOutputStream> writer; + + // We need to create a new pipe in order to read the aData and the rest of + // the input stream together in the old format. This would also help us with + // handling big endian correctly. + NS_NewPipe(getter_AddRefs(reader), getter_AddRefs(writer)); + + nsCOMPtr<nsIBinaryOutputStream> binaryPipeWriter = + NS_NewObjectOutputStream(writer); + + // Write back the aData so that we can read bytes from it and handle big + // endian correctly. + nsresult rv = binaryPipeWriter->Write32(aData); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIBinaryInputStream> binaryPipeReader = + NS_NewObjectInputStream(reader); + + rv = binaryPipeReader->ReadBoolean(&mSendReferrer); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool isComputed; + rv = binaryPipeReader->ReadBoolean(&isComputed); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // We need to handle the following string if isComputed is true. + if (isComputed) { + // Comsume the following 2 bytes from the input stream. They are the half + // part of the length prefix of the following string. + uint16_t data; + rv = aInputStream->Read16(&data); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Write the bytes to the pipe so that we can read the length of the string. + rv = binaryPipeWriter->Write16(data); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + uint32_t length; + rv = binaryPipeReader->Read32(&length); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Consume the string body from the input stream. + nsAutoCString computedReferrer; + rv = NS_ConsumeStream(aInputStream, length, computedReferrer); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + mComputedReferrer.emplace(computedReferrer); + + // Read the remaining two bytes and write to the pipe. + uint16_t remain; + rv = aInputStream->Read16(&remain); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = binaryPipeWriter->Write16(remain); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = binaryPipeReader->ReadBoolean(&mInitialized); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = binaryPipeReader->ReadBoolean(&mOverridePolicyByDefault); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +ReferrerInfo::Read(nsIObjectInputStream* aStream) { + bool nonNull; + nsresult rv = aStream->ReadBoolean(&nonNull); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (nonNull) { + nsAutoCString spec; + nsresult rv = aStream->ReadCString(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = NS_NewURI(getter_AddRefs(mOriginalReferrer), spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + mOriginalReferrer = nullptr; + } + + // ReferrerPolicy.webidl has different order with ReferrerPolicyIDL. We store + // to disk using the order of ReferrerPolicyIDL, so we convert to + // ReferrerPolicyIDL to make it be compatible to the old format. + uint32_t policy; + rv = aStream->Read32(&policy); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + mPolicy = ReferrerPolicyIDLToReferrerPolicy( + static_cast<nsIReferrerInfo::ReferrerPolicyIDL>(policy)); + + uint32_t originalPolicy; + rv = aStream->Read32(&originalPolicy); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1784045#c6 for more + // details. + // + // We need to differentiate the old format and the new format here in order + // to be able to read both formats. The check here helps us with verifying + // which format it is. + if (MOZ_UNLIKELY(originalPolicy > 0xFF)) { + mOriginalPolicy = mPolicy; + + return ReadTailDataBeforeGecko100(originalPolicy, aStream); + } + + mOriginalPolicy = ReferrerPolicyIDLToReferrerPolicy( + static_cast<nsIReferrerInfo::ReferrerPolicyIDL>(originalPolicy)); + + rv = aStream->ReadBoolean(&mSendReferrer); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool isComputed; + rv = aStream->ReadBoolean(&isComputed); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (isComputed) { + nsAutoCString computedReferrer; + rv = aStream->ReadCString(computedReferrer); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + mComputedReferrer.emplace(computedReferrer); + } + + rv = aStream->ReadBoolean(&mInitialized); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aStream->ReadBoolean(&mOverridePolicyByDefault); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +ReferrerInfo::Write(nsIObjectOutputStream* aStream) { + bool nonNull = (mOriginalReferrer != nullptr); + nsresult rv = aStream->WriteBoolean(nonNull); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (nonNull) { + nsAutoCString spec; + nsresult rv = mOriginalReferrer->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aStream->WriteStringZ(spec.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = aStream->Write32(ReferrerPolicyToReferrerPolicyIDL(mPolicy)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aStream->Write32(ReferrerPolicyToReferrerPolicyIDL(mOriginalPolicy)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aStream->WriteBoolean(mSendReferrer); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool isComputed = mComputedReferrer.isSome(); + rv = aStream->WriteBoolean(isComputed); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (isComputed) { + rv = aStream->WriteStringZ(mComputedReferrer.value().get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = aStream->WriteBoolean(mInitialized); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aStream->WriteBoolean(mOverridePolicyByDefault); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +void ReferrerInfo::RecordTelemetry(nsIHttpChannel* aChannel) { +#ifdef DEBUG + MOZ_ASSERT(!mTelemetryRecorded); + mTelemetryRecorded = true; +#endif // DEBUG + + // The telemetry probe has 18 buckets. The first 9 buckets are for same-site + // requests and the rest 9 buckets are for cross-site requests. + uint32_t telemetryOffset = + IsCrossSiteRequest(aChannel) + ? static_cast<uint32_t>(ReferrerPolicy::EndGuard_) + : 0; + + Telemetry::Accumulate(Telemetry::REFERRER_POLICY_COUNT, + static_cast<uint32_t>(mPolicy) + telemetryOffset); +} + +} // namespace mozilla::dom diff --git a/dom/security/ReferrerInfo.h b/dom/security/ReferrerInfo.h new file mode 100644 index 0000000000..79746678ac --- /dev/null +++ b/dom/security/ReferrerInfo.h @@ -0,0 +1,481 @@ +/* -*- 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_ReferrerInfo_h +#define mozilla_dom_ReferrerInfo_h + +#include "nsCOMPtr.h" +#include "nsIReferrerInfo.h" +#include "nsReadableUtils.h" +#include "mozilla/Maybe.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/dom/ReferrerPolicyBinding.h" + +#define REFERRERINFOF_CONTRACTID "@mozilla.org/referrer-info;1" +// 041a129f-10ce-4bda-a60d-e027a26d5ed0 +#define REFERRERINFO_CID \ + { \ + 0x041a129f, 0x10ce, 0x4bda, { \ + 0xa6, 0x0d, 0xe0, 0x27, 0xa2, 0x6d, 0x5e, 0xd0 \ + } \ + } + +class nsIHttpChannel; +class nsIURI; +class nsIChannel; +class nsILoadInfo; +class nsINode; +class nsIPrincipal; + +namespace mozilla { +class StyleSheet; +class URLAndReferrerInfo; + +namespace net { +class HttpBaseChannel; +class nsHttpChannel; +} // namespace net +} // namespace mozilla + +using mozilla::Maybe; + +namespace mozilla::dom { + +/** + * The ReferrerInfo class holds the raw referrer and potentially a referrer + * policy which allows to query the computed referrer which should be applied to + * a channel as the actual referrer value. + * + * The ReferrerInfo class solely contains readonly fields and represents a 1:1 + * sync to the referrer header of the corresponding channel. In turn that means + * the class is immutable - so any modifications require to clone the current + * ReferrerInfo. + * + * For example if a request undergoes a redirect, the new channel + * will need a new ReferrerInfo clone with members being updated accordingly. + */ + +class ReferrerInfo : public nsIReferrerInfo { + public: + typedef enum ReferrerPolicy ReferrerPolicyEnum; + ReferrerInfo(); + + explicit ReferrerInfo( + nsIURI* aOriginalReferrer, + ReferrerPolicyEnum aPolicy = ReferrerPolicy::_empty, + bool aSendReferrer = true, + const Maybe<nsCString>& aComputedReferrer = Maybe<nsCString>()); + + // Creates already initialized ReferrerInfo from an element or a document. + explicit ReferrerInfo(const Element&); + explicit ReferrerInfo(const Document&); + + // create an exact copy of the ReferrerInfo + already_AddRefed<ReferrerInfo> Clone() const; + + // create an copy of the ReferrerInfo with new referrer policy + already_AddRefed<ReferrerInfo> CloneWithNewPolicy( + ReferrerPolicyEnum aPolicy) const; + + // create an copy of the ReferrerInfo with new send referrer + already_AddRefed<ReferrerInfo> CloneWithNewSendReferrer( + bool aSendReferrer) const; + + // create an copy of the ReferrerInfo with new original referrer + already_AddRefed<ReferrerInfo> CloneWithNewOriginalReferrer( + nsIURI* aOriginalReferrer) const; + + // Record the telemetry for the referrer policy. + void RecordTelemetry(nsIHttpChannel* aChannel); + + /* + * Helper function to create a new ReferrerInfo object from other. We will not + * pass in any computed values and override referrer policy if needed + * + * @param aOther the other referrerInfo object to init from. + * @param aPolicyOverride referrer policy to override if necessary. + */ + static already_AddRefed<nsIReferrerInfo> CreateFromOtherAndPolicyOverride( + nsIReferrerInfo* aOther, ReferrerPolicyEnum aPolicyOverride); + + /* + * Helper function to create a new ReferrerInfo object from a given document + * and override referrer policy if needed (for example, when parsing link + * header or speculative loading). + * + * @param aDocument the document to init referrerInfo object. + * @param aPolicyOverride referrer policy to override if necessary. + */ + static already_AddRefed<nsIReferrerInfo> CreateFromDocumentAndPolicyOverride( + Document* aDoc, ReferrerPolicyEnum aPolicyOverride); + + /* + * Implements step 3.1 and 3.3 of the Determine request's Referrer algorithm + * from the Referrer Policy specification. + * + * https://w3c.github.io/webappsec/specs/referrer-policy/#determine-requests-referrer + */ + static already_AddRefed<nsIReferrerInfo> CreateForFetch( + nsIPrincipal* aPrincipal, Document* aDoc); + + /** + * Helper function to create new ReferrerInfo object from a given external + * stylesheet. The returned nsIReferrerInfo object will be used for any + * requests or resources referenced by the sheet. + * + * @param aSheet the stylesheet to init referrerInfo. + * @param aPolicy referrer policy from header if there's any. + */ + static already_AddRefed<nsIReferrerInfo> CreateForExternalCSSResources( + StyleSheet* aExternalSheet, + ReferrerPolicyEnum aPolicy = ReferrerPolicy::_empty); + + /** + * Helper function to create new ReferrerInfo object from a given document. + * The returned nsIReferrerInfo object will be used for any requests or + * resources referenced by internal stylesheet (for example style="" or + * wrapped by <style> tag). + * + * @param aDocument the document to init referrerInfo object. + */ + static already_AddRefed<nsIReferrerInfo> CreateForInternalCSSResources( + Document* aDocument); + + /** + * Helper function to create new ReferrerInfo object from a given document. + * The returned nsIReferrerInfo object will be used for any requests or + * resources referenced by SVG. + * + * @param aDocument the document to init referrerInfo object. + */ + static already_AddRefed<nsIReferrerInfo> CreateForSVGResources( + Document* aDocument); + + /** + * Check whether the given referrer's scheme is allowed to be computed and + * sent. The allowlist schemes are: http, https. + */ + static bool IsReferrerSchemeAllowed(nsIURI* aReferrer); + + /* + * The Referrer Policy should be inherited for nested browsing contexts that + * are not created from responses. Such as: srcdoc, data, blob. + */ + static bool ShouldResponseInheritReferrerInfo(nsIChannel* aChannel); + + /* + * Check whether referrer is allowed to send in secure to insecure scenario. + */ + static nsresult HandleSecureToInsecureReferral(nsIURI* aOriginalURI, + nsIURI* aURI, + ReferrerPolicyEnum aPolicy, + bool& aAllowed); + + /** + * Returns true if the given channel is cross-origin request + * + * Computing whether the request is cross-origin may be expensive, so please + * do that in cases where we're going to use this information later on. + */ + static bool IsCrossOriginRequest(nsIHttpChannel* aChannel); + + /** + * Returns true if the given channel is cross-site request. + */ + static bool IsCrossSiteRequest(nsIHttpChannel* aChannel); + + /** + * Returns true if the given channel is suppressed by Referrer-Policy header + * and should set "null" to Origin header. + */ + static bool ShouldSetNullOriginHeader(net::HttpBaseChannel* aChannel, + nsIURI* aOriginURI); + + /** + * Getter for network.http.sendRefererHeader. + */ + static uint32_t GetUserReferrerSendingPolicy(); + + /** + * Getter for network.http.referer.XOriginPolicy. + */ + static uint32_t GetUserXOriginSendingPolicy(); + + /** + * Getter for network.http.referer.trimmingPolicy. + */ + static uint32_t GetUserTrimmingPolicy(); + + /** + * Getter for network.http.referer.XOriginTrimmingPolicy. + */ + static uint32_t GetUserXOriginTrimmingPolicy(); + + /** + * Return default referrer policy which is controlled by user + * prefs: + * network.http.referer.defaultPolicy for regular mode + * network.http.referer.defaultPolicy.trackers for third-party trackers + * in regular mode + * network.http.referer.defaultPolicy.pbmode for private mode + * network.http.referer.defaultPolicy.trackers.pbmode for third-party trackers + * in private mode + */ + static ReferrerPolicyEnum GetDefaultReferrerPolicy( + nsIHttpChannel* aChannel = nullptr, nsIURI* aURI = nullptr, + bool aPrivateBrowsing = false); + + /** + * Return default referrer policy for third party which is controlled by user + * prefs: + * network.http.referer.defaultPolicy.trackers for regular mode + * network.http.referer.defaultPolicy.trackers.pbmode for private mode + */ + static ReferrerPolicyEnum GetDefaultThirdPartyReferrerPolicy( + bool aPrivateBrowsing = false); + + /* + * Helper function to parse ReferrerPolicy from meta tag referrer content. + * For example: <meta name="referrer" content="origin"> + * + * @param aContent content string to be transformed into ReferrerPolicyEnum, + * e.g. "origin". + */ + static ReferrerPolicyEnum ReferrerPolicyFromMetaString( + const nsAString& aContent); + + /* + * Helper function to parse ReferrerPolicy from string content of + * referrerpolicy attribute. + * For example: <a href="http://example.com" referrerpolicy="no-referrer"> + * + * @param aContent content string to be transformed into ReferrerPolicyEnum, + * e.g. "no-referrer". + */ + static ReferrerPolicyEnum ReferrerPolicyAttributeFromString( + const nsAString& aContent); + + /* + * Helper function to parse ReferrerPolicy from string content of + * Referrer-Policy header. + * For example: Referrer-Policy: origin no-referrer + * https://www.w3.org/tr/referrer-policy/#parse-referrer-policy-from-header + * + * @param aContent content string to be transformed into ReferrerPolicyEnum. + * e.g. "origin no-referrer" + */ + static ReferrerPolicyEnum ReferrerPolicyFromHeaderString( + const nsAString& aContent); + + /* + * Helper function to convert ReferrerPolicy enum to string + * + * @param aPolicy referrer policy to convert. + */ + static const char* ReferrerPolicyToString(ReferrerPolicyEnum aPolicy); + + /** + * Hash function for this object + */ + HashNumber Hash() const; + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREFERRERINFO + NS_DECL_NSISERIALIZABLE + + private: + virtual ~ReferrerInfo() = default; + + ReferrerInfo(const ReferrerInfo& rhs); + + /* + * Trimming policy when compute referrer, indicate how much information in the + * referrer will be sent. Order matters here. + */ + enum TrimmingPolicy : uint32_t { + ePolicyFullURI = 0, + ePolicySchemeHostPortPath = 1, + ePolicySchemeHostPort = 2, + }; + + /* + * Referrer sending policy, indicates type of action could trigger to send + * referrer header, not send at all, send only with user's action (click on a + * link) or send even with inline content request (image request). + * Order matters here. + */ + enum ReferrerSendingPolicy : uint32_t { + ePolicyNotSend = 0, + ePolicySendWhenUserTrigger = 1, + ePolicySendInlineContent = 2, + }; + + /* + * Sending referrer when cross origin policy, indicates when referrer should + * be send when compare 2 origins. Order matters here. + */ + enum XOriginSendingPolicy : uint32_t { + ePolicyAlwaysSend = 0, + ePolicySendWhenSameDomain = 1, + ePolicySendWhenSameHost = 2, + }; + + /* + * Handle user controlled pref network.http.referer.XOriginPolicy + */ + nsresult HandleUserXOriginSendingPolicy(nsIURI* aURI, nsIURI* aReferrer, + bool& aAllowed) const; + + /* + * Handle user controlled pref network.http.sendRefererHeader + */ + nsresult HandleUserReferrerSendingPolicy(nsIHttpChannel* aChannel, + bool& aAllowed) const; + + /* + * Compute trimming policy from user controlled prefs. + * This function is called when we already made sure a nonempty referrer is + * allowed to send. + */ + TrimmingPolicy ComputeTrimmingPolicy(nsIHttpChannel* aChannel) const; + + // HttpBaseChannel could access IsInitialized() and ComputeReferrer(); + friend class mozilla::net::HttpBaseChannel; + + /* + * Compute referrer for a given channel. The computation result then will be + * stored in this class and then used to set the actual referrer header of + * the channel. The computation could be controlled by several user prefs + * which are defined in StaticPrefList.yaml (see StaticPrefList.yaml for more + * details): + * network.http.sendRefererHeader + * network.http.referer.spoofSource + * network.http.referer.hideOnionSource + * network.http.referer.XOriginPolicy + * network.http.referer.trimmingPolicy + * network.http.referer.XOriginTrimmingPolicy + */ + nsresult ComputeReferrer(nsIHttpChannel* aChannel); + + /* + * Check whether the ReferrerInfo has been initialized or not. + */ + bool IsInitialized() { return mInitialized; } + + // nsHttpChannel, Document could access IsPolicyOverrided(); + friend class mozilla::net::nsHttpChannel; + friend class mozilla::dom::Document; + /* + * Check whether if unset referrer policy is overrided by default or not + */ + bool IsPolicyOverrided() { return mOverridePolicyByDefault; } + + /* + * Get origin string from a given valid referrer URI (http, https) + * + * @aReferrer - the full referrer URI + * @aResult - the resulting aReferrer in string format. + */ + nsresult GetOriginFromReferrerURI(nsIURI* aReferrer, + nsACString& aResult) const; + + /* + * Trim a given referrer with a given a trimming policy, + */ + nsresult TrimReferrerWithPolicy(nsIURI* aReferrer, + TrimmingPolicy aTrimmingPolicy, + nsACString& aResult) const; + + /** + * Returns true if we should ignore less restricted referrer policies, + * including 'unsafe_url', 'no_referrer_when_downgrade' and + * 'origin_when_cross_origin', for the given channel. We only apply this + * restriction for cross-site requests. For the same-site request, we will + * still allow overriding the default referrer policy with less restricted + * one. + * + * Note that the channel triggered by the system and the extension will be + * exempt from this restriction. + */ + bool ShouldIgnoreLessRestrictedPolicies( + nsIHttpChannel* aChannel, const ReferrerPolicyEnum aPolicy) const; + + /* + * Limit referrer length using the following ruleset: + * - If the length of referrer URL is over max length, strip down to origin. + * - If the origin is still over max length, remove the referrer entirely. + * + * This function comlements TrimReferrerPolicy and needs to be called right + * after TrimReferrerPolicy. + * + * @aChannel - used to query information needed for logging to the console. + * @aReferrer - the full referrer URI; needs to be identical to aReferrer + * passed to TrimReferrerPolicy. + * @aTrimmingPolicy - represents the trimming policy which was applied to the + * referrer; needs to be identical to aTrimmingPolicy + * passed to TrimReferrerPolicy. + * @aInAndOutTrimmedReferrer - an in and outgoing argument representing the + * referrer value. Please pass the result of + * TrimReferrerWithPolicy as + * aInAndOutTrimmedReferrer which will then be + * reduced to the origin or completely truncated + * in case the referrer value exceeds the length + * limitation. + */ + nsresult LimitReferrerLength(nsIHttpChannel* aChannel, nsIURI* aReferrer, + TrimmingPolicy aTrimmingPolicy, + nsACString& aInAndOutTrimmedReferrer) const; + + /** + * The helper function to read the old data format before gecko 100 for + * deserialization. + */ + nsresult ReadTailDataBeforeGecko100(const uint32_t& aData, + nsIObjectInputStream* aInputStream); + + /* + * Write message to the error console + */ + void LogMessageToConsole(nsIHttpChannel* aChannel, const char* aMsg, + const nsTArray<nsString>& aParams) const; + + friend class mozilla::URLAndReferrerInfo; + + nsCOMPtr<nsIURI> mOriginalReferrer; + + ReferrerPolicyEnum mPolicy; + + // The referrer policy that has been set originally for the channel. Note that + // the policy may have been overridden by the default referrer policy, so we + // need to keep track of this if we need to recover the original referrer + // policy. + ReferrerPolicyEnum mOriginalPolicy; + + // Indicates if the referrer should be sent or not even when it's available + // (default is true). + bool mSendReferrer; + + // Since the ReferrerInfo is immutable, we use this member as a helper to + // ensure no one can call e.g. init() twice to modify state of the + // ReferrerInfo. + bool mInitialized; + + // Indicates if unset referrer policy is overrided by default + bool mOverridePolicyByDefault; + + // Store a computed referrer for a given channel + Maybe<nsCString> mComputedReferrer; + +#ifdef DEBUG + // Indicates if the telemetry has been recorded. This is used to make sure the + // telemetry will be only recored once. + bool mTelemetryRecorded = false; +#endif // DEBUG +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ReferrerInfo_h diff --git a/dom/security/SRICheck.cpp b/dom/security/SRICheck.cpp new file mode 100644 index 0000000000..42e6a8c896 --- /dev/null +++ b/dom/security/SRICheck.cpp @@ -0,0 +1,490 @@ +/* -*- 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 "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsIConsoleReportCollector.h" +#include "nsIScriptError.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::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, "Sub-resource Integrity"_ns, + nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0, + "IneligibleResource"_ns, 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 + + // 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); + } + SRILOG(("SRICheck::IntegrityMetadata, metadataList=%s", metadataList.get())); + + // 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, "Sub-resource Integrity"_ns, + nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0, + "MalformedIntegrityHash"_ns, + 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, "Sub-resource Integrity"_ns, + nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0, + "UnsupportedHashAlg"_ns, + 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; +} + +////////////////////////////////////////////////////////////// +// +////////////////////////////////////////////////////////////// +SRICheckDataVerifier::SRICheckDataVerifier(const SRIMetadata& aMetadata, + const nsACString& aSourceFileURI, + nsIConsoleReportCollector* aReporter) + : mCryptoHash(nullptr), + mBytesHashed(0), + mHashLength(0), + mHashType('\0'), + mInvalidMetadata(false), + mComplete(false) { + MOZ_ASSERT(!aMetadata.IsEmpty()); // should be checked by caller + MOZ_ASSERT(aReporter); + + if (!aMetadata.IsValid()) { + nsTArray<nsString> params; + aReporter->AddConsoleReport( + nsIScriptError::warningFlag, "Sub-resource Integrity"_ns, + nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0, + "NoValidMetadata"_ns, 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; + } + + nsCOMPtr<nsICryptoHash> cryptoHash; + nsresult rv = NS_NewCryptoHash(mHashType, getter_AddRefs(cryptoHash)); + 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, "Sub-resource Integrity"_ns, + nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0, + "InvalidIntegrityBase64"_ns, + 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, "Sub-resource Integrity"_ns, + nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0, + "InvalidIntegrityLength"_ns, + 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 = aChannel; + request->GetName(requestURL); + SRILOG(("SRICheckDataVerifier::Verify, url=%s (length=%zu)", + requestURL.get(), mBytesHashed)); + } + + nsresult rv = Finish(); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + 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, "Sub-resource Integrity"_ns, + nsContentUtils::eSECURITY_PROPERTIES, aSourceFileURI, 0, 0, + "IntegrityMismatch"_ns, 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); + decltype(mHashLength) len = 0; + memcpy(&len, &aData[offset], sizeof(mHashLength)); + 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; + decltype(mHashType) hashType; + memcpy(&hashType, &aData[offset], sizeof(mHashType)); + if (hashType != mHashType) { + SRILOG( + ("SRICheckDataVerifier::ImportDataSummary, hash type[%d] does not " + "match[%d]", + hashType, mHashType)); + return NS_ERROR_SRI_UNEXPECTED_HASH_TYPE; + } + offset += sizeof(mHashType); + + decltype(mHashLength) hashLength; + memcpy(&hashLength, &aData[offset], sizeof(mHashLength)); + if (hashLength != mHashLength) { + SRILOG( + ("SRICheckDataVerifier::ImportDataSummary, hash length[%d] does not " + "match[%d]", + hashLength, 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; + memcpy(&aData[offset], &mHashType, sizeof(mHashType)); + offset += sizeof(mHashType); + memcpy(&aData[offset], &mHashLength, sizeof(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; + memset(&aData[offset], 0, sizeof(mHashType)); + offset += sizeof(mHashType); + memset(&aData[offset], 0, sizeof(mHashLength)); + 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 mozilla::dom diff --git a/dom/security/SRICheck.h b/dom/security/SRICheck.h new file mode 100644 index 0000000000..dca0fde633 --- /dev/null +++ b/dom/security/SRICheck.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 mozilla_dom_SRICheck_h +#define mozilla_dom_SRICheck_h + +#include "nsTString.h" +#include "nsStringFwd.h" +#include "nsCOMPtr.h" +#include "nsICryptoHash.h" + +class nsIChannel; +class nsIConsoleReportCollector; + +namespace mozilla::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); +}; + +// 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 mozilla::dom + +#endif // mozilla_dom_SRICheck_h diff --git a/dom/security/SRILogHelper.h b/dom/security/SRILogHelper.h new file mode 100644 index 0000000000..e453f30842 --- /dev/null +++ b/dom/security/SRILogHelper.h @@ -0,0 +1,24 @@ +/* -*- 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::dom { + +class SRILogHelper final { + public: + static LogModule* GetSriLog() { + static LazyLogModule gSriPRLog("SRI"); + return gSriPRLog; + } +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_SRILogHelper_h diff --git a/dom/security/SRIMetadata.cpp b/dom/security/SRIMetadata.cpp new file mode 100644 index 0000000000..02144f0f13 --- /dev/null +++ b/dom/security/SRIMetadata.cpp @@ -0,0 +1,187 @@ +/* -*- 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/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::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=%zu)", + 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; +} + +bool SRIMetadata::CanTrustBeDelegatedTo(const SRIMetadata& aOther) const { + if (IsEmpty()) { + // No integrity requirements enforced, just let go. + return true; + } + + if (aOther.IsEmpty()) { + // This metadata requires a check and the other has none, can't delegate. + return false; + } + + if (mAlgorithmType != aOther.mAlgorithmType) { + // They must use the same hash algorithm. + return false; + } + + // They must be completely identical, except for the order of hashes. + // We don't know which hash is the one passing eventually the check, so only + // option is to require this metadata to contain the same set of hashes as the + // one we want to delegate the trust to. + if (mHashes.Length() != aOther.mHashes.Length()) { + return false; + } + + for (const auto& hash : mHashes) { + if (!aOther.mHashes.Contains(hash)) { + return false; + } + } + + return true; +} + +} // namespace mozilla::dom diff --git a/dom/security/SRIMetadata.h b/dom/security/SRIMetadata.h new file mode 100644 index 0000000000..caa3ba25f3 --- /dev/null +++ b/dom/security/SRIMetadata.h @@ -0,0 +1,90 @@ +/* -*- 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::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; } + + // Return true if: + // - this integrity metadata is empty, or + // - the other integrity metadata has the same hash algorithm and also the + // same set of values otherwise, return false. + // + // This method simply checks if the other integrity metadata is identical to + // this one (if it exists), so that a load that has been checked against that + // other integrity metadata implies that the current integrity metadata is + // also satisfied. + bool CanTrustBeDelegatedTo(const SRIMetadata& aOther) const; + + private: + CopyableTArray<nsCString> mHashes; + nsString mIntegrityString; + nsCString mAlgorithm; + int8_t mAlgorithmType; + bool mEmpty; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_SRIMetadata_h diff --git a/dom/security/SecFetch.cpp b/dom/security/SecFetch.cpp new file mode 100644 index 0000000000..06aa3de699 --- /dev/null +++ b/dom/security/SecFetch.cpp @@ -0,0 +1,401 @@ +/* -*- 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 "SecFetch.h" +#include "nsIHttpChannel.h" +#include "nsContentUtils.h" +#include "nsIRedirectHistoryEntry.h" +#include "nsIReferrerInfo.h" +#include "mozIThirdPartyUtil.h" +#include "nsMixedContentBlocker.h" +#include "nsNetUtil.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/StaticPrefs_dom.h" + +// Helper function which maps an internal content policy type +// to the corresponding destination for the context of SecFetch. +nsCString MapInternalContentPolicyTypeToDest(nsContentPolicyType aType) { + switch (aType) { + case nsIContentPolicy::TYPE_OTHER: + return "empty"_ns; + case nsIContentPolicy::TYPE_INTERNAL_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_MODULE: + case nsIContentPolicy::TYPE_INTERNAL_MODULE_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS: + case nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_FRAME_MESSAGEMANAGER_SCRIPT: + case nsIContentPolicy::TYPE_SCRIPT: + return "script"_ns; + case nsIContentPolicy::TYPE_INTERNAL_WORKER: + return "worker"_ns; + case nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER: + return "sharedworker"_ns; + case nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER: + return "serviceworker"_ns; + case nsIContentPolicy::TYPE_INTERNAL_AUDIOWORKLET: + return "audioworklet"_ns; + case nsIContentPolicy::TYPE_INTERNAL_PAINTWORKLET: + return "paintworklet"_ns; + case nsIContentPolicy::TYPE_IMAGESET: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON: + case nsIContentPolicy::TYPE_IMAGE: + return "image"_ns; + case nsIContentPolicy::TYPE_STYLESHEET: + case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET: + case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET_PRELOAD: + return "style"_ns; + case nsIContentPolicy::TYPE_OBJECT: + case nsIContentPolicy::TYPE_INTERNAL_OBJECT: + return "object"_ns; + case nsIContentPolicy::TYPE_INTERNAL_EMBED: + return "embed"_ns; + case nsIContentPolicy::TYPE_DOCUMENT: + return "document"_ns; + case nsIContentPolicy::TYPE_SUBDOCUMENT: + case nsIContentPolicy::TYPE_INTERNAL_IFRAME: + return "iframe"_ns; + case nsIContentPolicy::TYPE_INTERNAL_FRAME: + return "frame"_ns; + case nsIContentPolicy::TYPE_PING: + return "empty"_ns; + case nsIContentPolicy::TYPE_XMLHTTPREQUEST: + case nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST: + return "empty"_ns; + case nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE: + return "empty"_ns; + case nsIContentPolicy::TYPE_OBJECT_SUBREQUEST: + return "empty"_ns; + case nsIContentPolicy::TYPE_DTD: + case nsIContentPolicy::TYPE_INTERNAL_DTD: + case nsIContentPolicy::TYPE_INTERNAL_FORCE_ALLOWED_DTD: + return "empty"_ns; + case nsIContentPolicy::TYPE_FONT: + case nsIContentPolicy::TYPE_INTERNAL_FONT_PRELOAD: + case nsIContentPolicy::TYPE_UA_FONT: + return "font"_ns; + case nsIContentPolicy::TYPE_MEDIA: + return "empty"_ns; + case nsIContentPolicy::TYPE_INTERNAL_AUDIO: + return "audio"_ns; + case nsIContentPolicy::TYPE_INTERNAL_VIDEO: + return "video"_ns; + case nsIContentPolicy::TYPE_INTERNAL_TRACK: + return "track"_ns; + case nsIContentPolicy::TYPE_WEBSOCKET: + return "websocket"_ns; + case nsIContentPolicy::TYPE_CSP_REPORT: + return "report"_ns; + case nsIContentPolicy::TYPE_XSLT: + return "xslt"_ns; + case nsIContentPolicy::TYPE_BEACON: + return "empty"_ns; + case nsIContentPolicy::TYPE_FETCH: + case nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD: + return "empty"_ns; + case nsIContentPolicy::TYPE_WEB_MANIFEST: + return "manifest"_ns; + case nsIContentPolicy::TYPE_SAVEAS_DOWNLOAD: + return "empty"_ns; + case nsIContentPolicy::TYPE_SPECULATIVE: + return "empty"_ns; + case nsIContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA: + return "empty"_ns; + case nsIContentPolicy::TYPE_WEB_IDENTITY: + return "webidentity"_ns; + case nsIContentPolicy::TYPE_INVALID: + break; + // Do not add default: so that compilers can catch the missing case. + } + + MOZ_CRASH("Unhandled nsContentPolicyType value"); +} + +// Helper function to determine if a ExpandedPrincipal is of the same-origin as +// a URI in the sec-fetch context. +void IsExpandedPrincipalSameOrigin( + nsCOMPtr<nsIExpandedPrincipal> aExpandedPrincipal, nsIURI* aURI, + bool* aRes) { + *aRes = false; + for (const auto& principal : aExpandedPrincipal->AllowList()) { + // Ignore extension principals to continue treating + // "moz-extension:"-requests as not "same-origin". + if (!mozilla::BasePrincipal::Cast(principal)->AddonPolicy()) { + // A ExpandedPrincipal usually has at most one ContentPrincipal, so we can + // check IsSameOrigin on it here and return early. + mozilla::BasePrincipal::Cast(principal)->IsSameOrigin(aURI, aRes); + return; + } + } +} + +// Helper function to determine whether a request (including involved +// redirects) is same-origin in the context of SecFetch. +bool IsSameOrigin(nsIHttpChannel* aHTTPChannel) { + nsCOMPtr<nsIURI> channelURI; + NS_GetFinalChannelURI(aHTTPChannel, getter_AddRefs(channelURI)); + + nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); + + if (mozilla::BasePrincipal::Cast(loadInfo->TriggeringPrincipal()) + ->AddonPolicy()) { + // If an extension triggered the load that has access to the URI then the + // load is considered as same-origin. + return mozilla::BasePrincipal::Cast(loadInfo->TriggeringPrincipal()) + ->AddonAllowsLoad(channelURI); + } + + bool isSameOrigin = false; + if (nsContentUtils::IsExpandedPrincipal(loadInfo->TriggeringPrincipal())) { + nsCOMPtr<nsIExpandedPrincipal> ep = + do_QueryInterface(loadInfo->TriggeringPrincipal()); + IsExpandedPrincipalSameOrigin(ep, channelURI, &isSameOrigin); + } else { + isSameOrigin = loadInfo->TriggeringPrincipal()->IsSameOrigin(channelURI); + } + + // if the initial request is not same-origin, we can return here + // because we already know it's not a same-origin request + if (!isSameOrigin) { + return false; + } + + // let's further check all the hoops in the redirectChain to + // ensure all involved redirects are same-origin + nsCOMPtr<nsIPrincipal> redirectPrincipal; + for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) { + entry->GetPrincipal(getter_AddRefs(redirectPrincipal)); + if (redirectPrincipal && !redirectPrincipal->IsSameOrigin(channelURI)) { + return false; + } + } + + // must be a same-origin request + return true; +} + +// Helper function to determine whether a request (including involved +// redirects) is same-site in the context of SecFetch. +bool IsSameSite(nsIChannel* aHTTPChannel) { + nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil = + do_GetService(THIRDPARTYUTIL_CONTRACTID); + if (!thirdPartyUtil) { + return false; + } + + nsAutoCString hostDomain; + nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); + nsresult rv = loadInfo->TriggeringPrincipal()->GetBaseDomain(hostDomain); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + nsAutoCString channelDomain; + nsCOMPtr<nsIURI> channelURI; + NS_GetFinalChannelURI(aHTTPChannel, getter_AddRefs(channelURI)); + rv = thirdPartyUtil->GetBaseDomain(channelURI, channelDomain); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + // if the initial request is not same-site, or not https, we can + // return here because we already know it's not a same-site request + if (!hostDomain.Equals(channelDomain) || + (!loadInfo->TriggeringPrincipal()->SchemeIs("https") && + !nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackHost( + hostDomain))) { + return false; + } + + // let's further check all the hoops in the redirectChain to + // ensure all involved redirects are same-site and https + nsCOMPtr<nsIPrincipal> redirectPrincipal; + for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) { + entry->GetPrincipal(getter_AddRefs(redirectPrincipal)); + if (redirectPrincipal) { + redirectPrincipal->GetBaseDomain(hostDomain); + if (!hostDomain.Equals(channelDomain) || + !redirectPrincipal->SchemeIs("https")) { + return false; + } + } + } + + // must be a same-site request + return true; +} + +// Helper function to determine whether a request was triggered +// by the end user in the context of SecFetch. +bool IsUserTriggeredForSecFetchSite(nsIHttpChannel* aHTTPChannel) { + /* + * The goal is to distinguish between "webby" navigations that are controlled + * by a given website (e.g. links, the window.location setter,form + * submissions, etc.), and those that are not (e.g. user interaction with a + * user agent’s address bar, bookmarks, etc). + */ + nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); + ExtContentPolicyType contentType = loadInfo->GetExternalContentPolicyType(); + + // A request issued by the browser is always user initiated. + if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal() && + contentType == ExtContentPolicy::TYPE_OTHER) { + return true; + } + + // only requests wich result in type "document" are subject to + // user initiated actions in the context of SecFetch. + if (contentType != ExtContentPolicy::TYPE_DOCUMENT && + contentType != ExtContentPolicy::TYPE_SUBDOCUMENT) { + return false; + } + + // The load is considered user triggered if it was triggered by an external + // application. + if (loadInfo->GetLoadTriggeredFromExternal()) { + return true; + } + + // sec-fetch-site can only be user triggered if the load was user triggered. + if (!loadInfo->GetHasValidUserGestureActivation()) { + return false; + } + + // We can assert that the navigation must be "webby" if the load was triggered + // by a meta refresh. See also Bug 1647128. + if (loadInfo->GetIsMetaRefresh()) { + return false; + } + + // All web requests have a valid "original" referrer set in the + // ReferrerInfo which we can use to determine whether a request + // was triggered by a user or not. + nsCOMPtr<nsIReferrerInfo> referrerInfo = aHTTPChannel->GetReferrerInfo(); + if (referrerInfo) { + nsCOMPtr<nsIURI> originalReferrer; + referrerInfo->GetOriginalReferrer(getter_AddRefs(originalReferrer)); + if (originalReferrer) { + return false; + } + } + + return true; +} + +void mozilla::dom::SecFetch::AddSecFetchDest(nsIHttpChannel* aHTTPChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); + nsContentPolicyType contentType = loadInfo->InternalContentPolicyType(); + nsCString dest = MapInternalContentPolicyTypeToDest(contentType); + + nsresult rv = + aHTTPChannel->SetRequestHeader("Sec-Fetch-Dest"_ns, dest, false); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); +} + +void mozilla::dom::SecFetch::AddSecFetchMode(nsIHttpChannel* aHTTPChannel) { + nsAutoCString mode("no-cors"); + + nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); + uint32_t securityMode = loadInfo->GetSecurityMode(); + ExtContentPolicyType externalType = loadInfo->GetExternalContentPolicyType(); + + if (securityMode == + nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT || + securityMode == nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED) { + mode = "same-origin"_ns; + } else if (securityMode == + nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) { + mode = "cors"_ns; + } else { + // If it's not one of the security modes above, then we ensure it's + // at least one of the others defined in nsILoadInfo + MOZ_ASSERT( + securityMode == + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT || + securityMode == + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + "unhandled security mode"); + } + + if (externalType == ExtContentPolicy::TYPE_DOCUMENT || + externalType == ExtContentPolicy::TYPE_SUBDOCUMENT || + externalType == ExtContentPolicy::TYPE_OBJECT) { + mode = "navigate"_ns; + } else if (externalType == ExtContentPolicy::TYPE_WEBSOCKET) { + mode = "websocket"_ns; + } + + nsresult rv = + aHTTPChannel->SetRequestHeader("Sec-Fetch-Mode"_ns, mode, false); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); +} + +void mozilla::dom::SecFetch::AddSecFetchSite(nsIHttpChannel* aHTTPChannel) { + nsAutoCString site("same-origin"); + + bool isSameOrigin = IsSameOrigin(aHTTPChannel); + if (!isSameOrigin) { + bool isSameSite = IsSameSite(aHTTPChannel); + if (isSameSite) { + site = "same-site"_ns; + } else { + site = "cross-site"_ns; + } + } + + if (IsUserTriggeredForSecFetchSite(aHTTPChannel)) { + site = "none"_ns; + } + + nsresult rv = + aHTTPChannel->SetRequestHeader("Sec-Fetch-Site"_ns, site, false); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); +} + +void mozilla::dom::SecFetch::AddSecFetchUser(nsIHttpChannel* aHTTPChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); + ExtContentPolicyType externalType = loadInfo->GetExternalContentPolicyType(); + + // sec-fetch-user only applies to loads of type document or subdocument + if (externalType != ExtContentPolicy::TYPE_DOCUMENT && + externalType != ExtContentPolicy::TYPE_SUBDOCUMENT) { + return; + } + + // sec-fetch-user only applies if the request is user triggered. + // requests triggered by an external application are considerd user triggered. + if (!loadInfo->GetLoadTriggeredFromExternal() && + !loadInfo->GetHasValidUserGestureActivation()) { + return; + } + + nsAutoCString user("?1"); + nsresult rv = + aHTTPChannel->SetRequestHeader("Sec-Fetch-User"_ns, user, false); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); +} + +void mozilla::dom::SecFetch::AddSecFetchHeader(nsIHttpChannel* aHTTPChannel) { + // if sec-fetch-* is prefed off, then there is nothing to do + if (!StaticPrefs::dom_security_secFetch_enabled()) { + return; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = aHTTPChannel->GetURI(getter_AddRefs(uri)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // if we are not dealing with a potentially trustworthy URL, then + // there is nothing to do here + if (!nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(uri)) { + return; + } + + AddSecFetchDest(aHTTPChannel); + AddSecFetchMode(aHTTPChannel); + AddSecFetchSite(aHTTPChannel); + AddSecFetchUser(aHTTPChannel); +} diff --git a/dom/security/SecFetch.h b/dom/security/SecFetch.h new file mode 100644 index 0000000000..b4b1495f4d --- /dev/null +++ b/dom/security/SecFetch.h @@ -0,0 +1,27 @@ +/* -*- 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_SecFetch_h +#define mozilla_dom_SecFetch_h + +class nsIHttpChannel; + +namespace mozilla::dom { + +class SecFetch final { + public: + static void AddSecFetchHeader(nsIHttpChannel* aHTTPChannel); + + private: + static void AddSecFetchDest(nsIHttpChannel* aHTTPChannel); + static void AddSecFetchMode(nsIHttpChannel* aHTTPChannel); + static void AddSecFetchSite(nsIHttpChannel* aHTTPChannel); + static void AddSecFetchUser(nsIHttpChannel* aHTTPChannel); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_SecFetch_h diff --git a/dom/security/featurepolicy/Feature.cpp b/dom/security/featurepolicy/Feature.cpp new file mode 100644 index 0000000000..92a92369da --- /dev/null +++ b/dom/security/featurepolicy/Feature.cpp @@ -0,0 +1,76 @@ +/* -*- 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 "Feature.h" +#include "mozilla/BasePrincipal.h" + +namespace mozilla::dom { + +void Feature::GetAllowList(nsTArray<nsCOMPtr<nsIPrincipal>>& aList) const { + MOZ_ASSERT(mPolicy == eAllowList); + aList.AppendElements(mAllowList); +} + +bool Feature::Allows(nsIPrincipal* aPrincipal) const { + if (mPolicy == eNone) { + return false; + } + + if (mPolicy == eAll) { + return true; + } + + return AllowListContains(aPrincipal); +} + +Feature::Feature(const nsAString& aFeatureName) + : mFeatureName(aFeatureName), mPolicy(eAllowList) {} + +Feature::~Feature() = default; + +const nsAString& Feature::Name() const { return mFeatureName; } + +void Feature::SetAllowsNone() { + mPolicy = eNone; + mAllowList.Clear(); +} + +bool Feature::AllowsNone() const { return mPolicy == eNone; } + +void Feature::SetAllowsAll() { + mPolicy = eAll; + mAllowList.Clear(); +} + +bool Feature::AllowsAll() const { return mPolicy == eAll; } + +void Feature::AppendToAllowList(nsIPrincipal* aPrincipal) { + MOZ_ASSERT(aPrincipal); + + mPolicy = eAllowList; + mAllowList.AppendElement(aPrincipal); +} + +bool Feature::AllowListContains(nsIPrincipal* aPrincipal) const { + MOZ_ASSERT(aPrincipal); + + if (!HasAllowList()) { + return false; + } + + for (nsIPrincipal* principal : mAllowList) { + if (BasePrincipal::Cast(principal)->Subsumes( + aPrincipal, BasePrincipal::ConsiderDocumentDomain)) { + return true; + } + } + + return false; +} + +bool Feature::HasAllowList() const { return mPolicy == eAllowList; } + +} // namespace mozilla::dom diff --git a/dom/security/featurepolicy/Feature.h b/dom/security/featurepolicy/Feature.h new file mode 100644 index 0000000000..e8a3d89c81 --- /dev/null +++ b/dom/security/featurepolicy/Feature.h @@ -0,0 +1,65 @@ +/* -*- 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_Feature_h +#define mozilla_dom_Feature_h + +#include "nsString.h" +#include "nsTArray.h" +#include "nsCOMPtr.h" + +class nsIPrincipal; + +namespace mozilla::dom { + +class Feature final { + public: + explicit Feature(const nsAString& aFeatureName); + + ~Feature(); + + const nsAString& Name() const; + + void SetAllowsNone(); + + bool AllowsNone() const; + + void SetAllowsAll(); + + bool AllowsAll() const; + + void AppendToAllowList(nsIPrincipal* aPrincipal); + + void GetAllowList(nsTArray<nsCOMPtr<nsIPrincipal>>& aList) const; + + bool AllowListContains(nsIPrincipal* aPrincipal) const; + + bool HasAllowList() const; + + bool Allows(nsIPrincipal* aPrincipal) const; + + private: + nsString mFeatureName; + + enum Policy { + // denotes a policy of "feature 'none'" + eNone, + + // denotes a policy of "feature *" + eAll, + + // denotes a policy of "feature bar.com foo.com" + eAllowList, + }; + + Policy mPolicy; + + CopyableTArray<nsCOMPtr<nsIPrincipal>> mAllowList; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_Feature_h diff --git a/dom/security/featurepolicy/FeaturePolicy.cpp b/dom/security/featurepolicy/FeaturePolicy.cpp new file mode 100644 index 0000000000..cb5c1ea44a --- /dev/null +++ b/dom/security/featurepolicy/FeaturePolicy.cpp @@ -0,0 +1,334 @@ +/* -*- 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 "FeaturePolicy.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/Feature.h" +#include "mozilla/dom/FeaturePolicyBinding.h" +#include "mozilla/dom/FeaturePolicyParser.h" +#include "mozilla/dom/FeaturePolicyUtils.h" +#include "mozilla/StaticPrefs_dom.h" +#include "nsContentUtils.h" +#include "nsNetUtil.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FeaturePolicy) +NS_IMPL_CYCLE_COLLECTING_ADDREF(FeaturePolicy) +NS_IMPL_CYCLE_COLLECTING_RELEASE(FeaturePolicy) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FeaturePolicy) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +FeaturePolicy::FeaturePolicy(nsINode* aNode) : mParentNode(aNode) {} + +void FeaturePolicy::InheritPolicy(FeaturePolicy* aParentPolicy) { + MOZ_ASSERT(aParentPolicy); + + mInheritedDeniedFeatureNames.Clear(); + + RefPtr<FeaturePolicy> dest = this; + RefPtr<FeaturePolicy> src = aParentPolicy; + + // Inherit origins which explicitly declared policy in chain + for (const Feature& featureInChain : + aParentPolicy->mDeclaredFeaturesInAncestorChain) { + dest->AppendToDeclaredAllowInAncestorChain(featureInChain); + } + + FeaturePolicyUtils::ForEachFeature([dest, src](const char* aFeatureName) { + nsString featureName; + featureName.AppendASCII(aFeatureName); + // Store unsafe allows all (allow=*) + if (src->HasFeatureUnsafeAllowsAll(featureName)) { + dest->mParentAllowedAllFeatures.AppendElement(featureName); + } + + // If the destination has a declared feature (via the HTTP header or 'allow' + // attribute) we allow the feature if the destination allows it and the + // parent allows its origin or the destinations' one. + if (dest->HasDeclaredFeature(featureName) && + dest->AllowsFeatureInternal(featureName, dest->mDefaultOrigin)) { + if (!src->AllowsFeatureInternal(featureName, src->mDefaultOrigin) && + !src->AllowsFeatureInternal(featureName, dest->mDefaultOrigin)) { + dest->SetInheritedDeniedFeature(featureName); + } + return; + } + + // If there was not a declared feature, we allow the feature if the parent + // FeaturePolicy allows the current origin. + if (!src->AllowsFeatureInternal(featureName, dest->mDefaultOrigin)) { + dest->SetInheritedDeniedFeature(featureName); + } + }); +} + +void FeaturePolicy::SetInheritedDeniedFeature(const nsAString& aFeatureName) { + MOZ_ASSERT(!HasInheritedDeniedFeature(aFeatureName)); + mInheritedDeniedFeatureNames.AppendElement(aFeatureName); +} + +bool FeaturePolicy::HasInheritedDeniedFeature( + const nsAString& aFeatureName) const { + return mInheritedDeniedFeatureNames.Contains(aFeatureName); +} + +bool FeaturePolicy::HasDeclaredFeature(const nsAString& aFeatureName) const { + for (const Feature& feature : mFeatures) { + if (feature.Name().Equals(aFeatureName)) { + return true; + } + } + + return false; +} + +bool FeaturePolicy::HasFeatureUnsafeAllowsAll( + const nsAString& aFeatureName) const { + for (const Feature& feature : mFeatures) { + if (feature.AllowsAll() && feature.Name().Equals(aFeatureName)) { + return true; + } + } + + // We should look into parent too (for example, document of iframe which + // allows all, would be unsafe) + return mParentAllowedAllFeatures.Contains(aFeatureName); +} + +void FeaturePolicy::AppendToDeclaredAllowInAncestorChain( + const Feature& aFeature) { + for (Feature& featureInChain : mDeclaredFeaturesInAncestorChain) { + if (featureInChain.Name().Equals(aFeature.Name())) { + MOZ_ASSERT(featureInChain.HasAllowList()); + + nsTArray<nsCOMPtr<nsIPrincipal>> list; + aFeature.GetAllowList(list); + + for (nsIPrincipal* principal : list) { + featureInChain.AppendToAllowList(principal); + } + continue; + } + } + + mDeclaredFeaturesInAncestorChain.AppendElement(aFeature); +} + +bool FeaturePolicy::IsSameOriginAsSrc(nsIPrincipal* aPrincipal) const { + MOZ_ASSERT(aPrincipal); + + if (!mSrcOrigin) { + return false; + } + + return BasePrincipal::Cast(mSrcOrigin) + ->Subsumes(aPrincipal, BasePrincipal::ConsiderDocumentDomain); +} + +void FeaturePolicy::SetDeclaredPolicy(Document* aDocument, + const nsAString& aPolicyString, + nsIPrincipal* aSelfOrigin, + nsIPrincipal* aSrcOrigin) { + ResetDeclaredPolicy(); + + mDeclaredString = aPolicyString; + mSelfOrigin = aSelfOrigin; + mSrcOrigin = aSrcOrigin; + + Unused << NS_WARN_IF(!FeaturePolicyParser::ParseString( + aPolicyString, aDocument, aSelfOrigin, aSrcOrigin, mFeatures)); + + // Only store explicitly declared allowlist + for (const Feature& feature : mFeatures) { + if (feature.HasAllowList()) { + AppendToDeclaredAllowInAncestorChain(feature); + } + } +} + +void FeaturePolicy::ResetDeclaredPolicy() { + mFeatures.Clear(); + mDeclaredString.Truncate(); + mSelfOrigin = nullptr; + mSrcOrigin = nullptr; + mDeclaredFeaturesInAncestorChain.Clear(); + mAttributeEnabledFeatureNames.Clear(); +} + +JSObject* FeaturePolicy::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return FeaturePolicy_Binding::Wrap(aCx, this, aGivenProto); +} + +bool FeaturePolicy::AllowsFeature(const nsAString& aFeatureName, + const Optional<nsAString>& aOrigin) const { + nsCOMPtr<nsIPrincipal> origin; + if (aOrigin.WasPassed()) { + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aOrigin.Value()); + if (NS_FAILED(rv)) { + return false; + } + origin = BasePrincipal::CreateContentPrincipal( + uri, BasePrincipal::Cast(mDefaultOrigin)->OriginAttributesRef()); + } else { + origin = mDefaultOrigin; + } + + if (NS_WARN_IF(!origin)) { + return false; + } + + return AllowsFeatureInternal(aFeatureName, origin); +} + +bool FeaturePolicy::AllowsFeatureExplicitlyInAncestorChain( + const nsAString& aFeatureName, nsIPrincipal* aOrigin) const { + MOZ_ASSERT(aOrigin); + + for (const Feature& feature : mDeclaredFeaturesInAncestorChain) { + if (feature.Name().Equals(aFeatureName)) { + return feature.AllowListContains(aOrigin); + } + } + + return false; +} + +bool FeaturePolicy::AllowsFeatureInternal(const nsAString& aFeatureName, + nsIPrincipal* aOrigin) const { + MOZ_ASSERT(aOrigin); + + // Let's see if have to disable this feature because inherited policy. + if (HasInheritedDeniedFeature(aFeatureName)) { + return false; + } + + for (const Feature& feature : mFeatures) { + if (feature.Name().Equals(aFeatureName)) { + return feature.Allows(aOrigin); + } + } + + switch (FeaturePolicyUtils::DefaultAllowListFeature(aFeatureName)) { + case FeaturePolicyUtils::FeaturePolicyValue::eAll: + return true; + + case FeaturePolicyUtils::FeaturePolicyValue::eSelf: + return BasePrincipal::Cast(mDefaultOrigin) + ->Subsumes(aOrigin, BasePrincipal::ConsiderDocumentDomain); + + case FeaturePolicyUtils::FeaturePolicyValue::eNone: + return false; + + default: + MOZ_CRASH("Unknown default value"); + } + + return false; +} + +void FeaturePolicy::Features(nsTArray<nsString>& aFeatures) { + RefPtr<FeaturePolicy> self = this; + FeaturePolicyUtils::ForEachFeature( + [self, &aFeatures](const char* aFeatureName) { + nsString featureName; + featureName.AppendASCII(aFeatureName); + aFeatures.AppendElement(featureName); + }); +} + +void FeaturePolicy::AllowedFeatures(nsTArray<nsString>& aAllowedFeatures) { + RefPtr<FeaturePolicy> self = this; + FeaturePolicyUtils::ForEachFeature( + [self, &aAllowedFeatures](const char* aFeatureName) { + nsString featureName; + featureName.AppendASCII(aFeatureName); + + if (self->AllowsFeatureInternal(featureName, self->mDefaultOrigin)) { + aAllowedFeatures.AppendElement(featureName); + } + }); +} + +void FeaturePolicy::GetAllowlistForFeature(const nsAString& aFeatureName, + nsTArray<nsString>& aList) const { + if (!AllowsFeatureInternal(aFeatureName, mDefaultOrigin)) { + return; + } + + for (const Feature& feature : mFeatures) { + if (feature.Name().Equals(aFeatureName)) { + if (feature.AllowsAll()) { + aList.AppendElement(u"*"_ns); + return; + } + + nsTArray<nsCOMPtr<nsIPrincipal>> list; + feature.GetAllowList(list); + + for (nsIPrincipal* principal : list) { + nsAutoCString originNoSuffix; + nsresult rv = principal->GetOriginNoSuffix(originNoSuffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + aList.AppendElement(NS_ConvertUTF8toUTF16(originNoSuffix)); + } + return; + } + } + + switch (FeaturePolicyUtils::DefaultAllowListFeature(aFeatureName)) { + case FeaturePolicyUtils::FeaturePolicyValue::eAll: + aList.AppendElement(u"*"_ns); + return; + + case FeaturePolicyUtils::FeaturePolicyValue::eSelf: { + nsAutoCString originNoSuffix; + nsresult rv = mDefaultOrigin->GetOriginNoSuffix(originNoSuffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + aList.AppendElement(NS_ConvertUTF8toUTF16(originNoSuffix)); + return; + } + + case FeaturePolicyUtils::FeaturePolicyValue::eNone: + return; + + default: + MOZ_CRASH("Unknown default value"); + } +} + +void FeaturePolicy::MaybeSetAllowedPolicy(const nsAString& aFeatureName) { + MOZ_ASSERT(FeaturePolicyUtils::IsSupportedFeature(aFeatureName) || + FeaturePolicyUtils::IsExperimentalFeature(aFeatureName)); + // Skip if feature is in experimental phase + if (!StaticPrefs::dom_security_featurePolicy_experimental_enabled() && + FeaturePolicyUtils::IsExperimentalFeature(aFeatureName)) { + return; + } + + if (HasDeclaredFeature(aFeatureName)) { + return; + } + + Feature feature(aFeatureName); + feature.SetAllowsAll(); + + mFeatures.AppendElement(feature); + mAttributeEnabledFeatureNames.AppendElement(aFeatureName); +} + +} // namespace mozilla::dom diff --git a/dom/security/featurepolicy/FeaturePolicy.h b/dom/security/featurepolicy/FeaturePolicy.h new file mode 100644 index 0000000000..65f5259749 --- /dev/null +++ b/dom/security/featurepolicy/FeaturePolicy.h @@ -0,0 +1,204 @@ +/* -*- 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_FeaturePolicy_h +#define mozilla_dom_FeaturePolicy_h + +#include "nsCycleCollectionParticipant.h" +#include "nsIPrincipal.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" + +/** + * FeaturePolicy + * ~~~~~~~~~~~~~ + * + * Each document and each HTMLIFrameElement have a FeaturePolicy object which is + * used to allow or deny features in their contexts. + * + * FeaturePolicy is composed by a set of directives configured by the + * 'Feature-Policy' HTTP Header and the 'allow' attribute in HTMLIFrameElements. + * Both header and attribute are parsed by FeaturePolicyParser which returns an + * array of Feature objects. Each Feature object has a feature name and one of + * these policies: + * - eNone - the feature is fully disabled. + * - eAll - the feature is allowed. + * - eAllowList - the feature is allowed for a list of origins. + * + * An interesting element of FeaturePolicy is the inheritance: each context + * inherits the feature-policy directives from the parent context, if it exists. + * When a context inherits a policy for feature X, it only knows if that feature + * is allowed or denied (it ignores the list of allowed origins for instance). + * This information is stored in an array of inherited feature strings because + * we care only to know when they are denied. + * + * FeaturePolicy can be reset if the 'allow' or 'src' attributes change in + * HTMLIFrameElements. 'src' attribute is important to compute correcly + * the features via FeaturePolicy 'src' keyword. + * + * When FeaturePolicy must decide if feature X is allowed or denied for the + * current origin, it checks if the parent context denied that feature. + * If not, it checks if there is a Feature object for that + * feature named X and if the origin is allowed or not. + * + * From a C++ point of view, use FeaturePolicyUtils to obtain the list of + * features and to check if they are allowed in the current context. + * + * dom.security.featurePolicy.header.enabled pref can be used to disable the + * HTTP header support. + **/ + +class nsINode; + +namespace mozilla::dom { +class Document; +class Feature; +template <typename T> +class Optional; + +class FeaturePolicyUtils; + +class FeaturePolicy final : public nsISupports, public nsWrapperCache { + friend class FeaturePolicyUtils; + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FeaturePolicy) + + explicit FeaturePolicy(nsINode* aNode); + + // A FeaturePolicy must have a default origin. + // This method must be called before any other exposed WebIDL method or before + // checking if a feature is allowed. + void SetDefaultOrigin(nsIPrincipal* aPrincipal) { + mDefaultOrigin = aPrincipal; + } + + void SetSrcOrigin(nsIPrincipal* aPrincipal) { mSrcOrigin = aPrincipal; } + + nsIPrincipal* DefaultOrigin() const { return mDefaultOrigin; } + + // Inherits the policy from the 'parent' context if it exists. + void InheritPolicy(FeaturePolicy* aParentFeaturePolicy); + + // Sets the declarative part of the policy. This can be from the HTTP header + // or for the 'allow' HTML attribute. + void SetDeclaredPolicy(mozilla::dom::Document* aDocument, + const nsAString& aPolicyString, + nsIPrincipal* aSelfOrigin, nsIPrincipal* aSrcOrigin); + + // This method creates a policy for aFeatureName allowing it to '*' if it + // doesn't exist yet. It's used by HTMLIFrameElement to enable features by + // attributes. + void MaybeSetAllowedPolicy(const nsAString& aFeatureName); + + // Clears all the declarative policy directives. This is needed when the + // 'allow' attribute or the 'src' attribute change for HTMLIFrameElement's + // policy. + void ResetDeclaredPolicy(); + + // This method appends a feature to in-chain declared allowlist. If the name's + // feature existed in the list, we only need to append the allowlist of new + // feature to the existed one. + void AppendToDeclaredAllowInAncestorChain(const Feature& aFeature); + + // This method returns true if aFeatureName is declared as "*" (allow all) + // in parent. + bool HasFeatureUnsafeAllowsAll(const nsAString& aFeatureName) const; + + // This method returns true if the aFeatureName is allowed for aOrigin + // explicitly in ancestor chain, + bool AllowsFeatureExplicitlyInAncestorChain(const nsAString& aFeatureName, + nsIPrincipal* aOrigin) const; + + bool IsSameOriginAsSrc(nsIPrincipal* aPrincipal) const; + + // WebIDL internal methods. + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsINode* GetParentObject() const { return mParentNode; } + + // WebIDL explosed methods. + + bool AllowsFeature(const nsAString& aFeatureName, + const Optional<nsAString>& aOrigin) const; + + void Features(nsTArray<nsString>& aFeatures); + + void AllowedFeatures(nsTArray<nsString>& aAllowedFeatures); + + void GetAllowlistForFeature(const nsAString& aFeatureName, + nsTArray<nsString>& aList) const; + + const nsTArray<nsString>& InheritedDeniedFeatureNames() const { + return mInheritedDeniedFeatureNames; + } + + const nsTArray<nsString>& AttributeEnabledFeatureNames() const { + return mAttributeEnabledFeatureNames; + } + + void SetInheritedDeniedFeatureNames( + const nsTArray<nsString>& aInheritedDeniedFeatureNames) { + mInheritedDeniedFeatureNames = aInheritedDeniedFeatureNames.Clone(); + } + + const nsAString& DeclaredString() const { return mDeclaredString; } + + nsIPrincipal* GetSelfOrigin() const { return mSelfOrigin; } + nsIPrincipal* GetSrcOrigin() const { return mSrcOrigin; } + + private: + ~FeaturePolicy() = default; + + // This method returns true if the aFeatureName is allowed for aOrigin, + // following the feature-policy directives. See the comment at the top of this + // file. + bool AllowsFeatureInternal(const nsAString& aFeatureName, + nsIPrincipal* aOrigin) const; + + // Inherits a single denied feature from the parent context. + void SetInheritedDeniedFeature(const nsAString& aFeatureName); + + bool HasInheritedDeniedFeature(const nsAString& aFeatureName) const; + + // This returns true if we have a declared feature policy for aFeatureName. + bool HasDeclaredFeature(const nsAString& aFeatureName) const; + + nsINode* mParentNode; + + // This is set in sub-contexts when the parent blocks some feature for the + // current context. + nsTArray<nsString> mInheritedDeniedFeatureNames; + + // The list of features that have been enabled via MaybeSetAllowedPolicy. + nsTArray<nsString> mAttributeEnabledFeatureNames; + + // This is set of feature names when the parent allows all for that feature. + nsTArray<nsString> mParentAllowedAllFeatures; + + // The explicitly declared policy contains allowlist as a set of origins + // except 'none' and '*'. This set contains all explicitly declared policies + // in ancestor chain + nsTArray<Feature> mDeclaredFeaturesInAncestorChain; + + // Feature policy for the current context. + nsTArray<Feature> mFeatures; + + // Declared string represents Feature policy. + nsString mDeclaredString; + + nsCOMPtr<nsIPrincipal> mDefaultOrigin; + nsCOMPtr<nsIPrincipal> mSelfOrigin; + nsCOMPtr<nsIPrincipal> mSrcOrigin; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FeaturePolicy_h diff --git a/dom/security/featurepolicy/FeaturePolicyParser.cpp b/dom/security/featurepolicy/FeaturePolicyParser.cpp new file mode 100644 index 0000000000..8ab95420aa --- /dev/null +++ b/dom/security/featurepolicy/FeaturePolicyParser.cpp @@ -0,0 +1,157 @@ +/* -*- 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 "FeaturePolicyParser.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/Feature.h" +#include "mozilla/dom/FeaturePolicyUtils.h" +#include "mozilla/dom/PolicyTokenizer.h" +#include "nsIScriptError.h" +#include "nsIURI.h" +#include "nsNetUtil.h" + +namespace mozilla::dom { + +namespace { + +void ReportToConsoleUnsupportedFeature(Document* aDocument, + const nsString& aFeatureName) { + if (!aDocument) { + return; + } + + AutoTArray<nsString, 1> params = {aFeatureName}; + + nsContentUtils::ReportToConsole( + nsIScriptError::warningFlag, "Feature Policy"_ns, aDocument, + nsContentUtils::eSECURITY_PROPERTIES, + "FeaturePolicyUnsupportedFeatureName", params); +} + +void ReportToConsoleInvalidEmptyAllowValue(Document* aDocument, + const nsString& aFeatureName) { + if (!aDocument) { + return; + } + + AutoTArray<nsString, 1> params = {aFeatureName}; + + nsContentUtils::ReportToConsole( + nsIScriptError::warningFlag, "Feature Policy"_ns, aDocument, + nsContentUtils::eSECURITY_PROPERTIES, + "FeaturePolicyInvalidEmptyAllowValue", params); +} + +void ReportToConsoleInvalidAllowValue(Document* aDocument, + const nsString& aValue) { + if (!aDocument) { + return; + } + + AutoTArray<nsString, 1> params = {aValue}; + + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + "Feature Policy"_ns, aDocument, + nsContentUtils::eSECURITY_PROPERTIES, + "FeaturePolicyInvalidAllowValue", params); +} + +} // namespace + +/* static */ +bool FeaturePolicyParser::ParseString(const nsAString& aPolicy, + Document* aDocument, + nsIPrincipal* aSelfOrigin, + nsIPrincipal* aSrcOrigin, + nsTArray<Feature>& aParsedFeatures) { + MOZ_ASSERT(aSelfOrigin); + + nsTArray<CopyableTArray<nsString>> tokens; + PolicyTokenizer::tokenizePolicy(aPolicy, tokens); + + nsTArray<Feature> parsedFeatures; + + for (const nsTArray<nsString>& featureTokens : tokens) { + if (featureTokens.IsEmpty()) { + continue; + } + + if (!FeaturePolicyUtils::IsSupportedFeature(featureTokens[0])) { + ReportToConsoleUnsupportedFeature(aDocument, featureTokens[0]); + continue; + } + + Feature feature(featureTokens[0]); + + if (featureTokens.Length() == 1) { + if (aSrcOrigin) { + feature.AppendToAllowList(aSrcOrigin); + } else { + ReportToConsoleInvalidEmptyAllowValue(aDocument, featureTokens[0]); + continue; + } + } else { + // we gotta start at 1 here + for (uint32_t i = 1; i < featureTokens.Length(); ++i) { + const nsString& curVal = featureTokens[i]; + if (curVal.LowerCaseEqualsASCII("'none'")) { + feature.SetAllowsNone(); + break; + } + + if (curVal.EqualsLiteral("*")) { + feature.SetAllowsAll(); + break; + } + + if (curVal.LowerCaseEqualsASCII("'self'")) { + feature.AppendToAllowList(aSelfOrigin); + continue; + } + + if (aSrcOrigin && curVal.LowerCaseEqualsASCII("'src'")) { + feature.AppendToAllowList(aSrcOrigin); + continue; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), curVal); + if (NS_FAILED(rv)) { + ReportToConsoleInvalidAllowValue(aDocument, curVal); + continue; + } + + nsCOMPtr<nsIPrincipal> origin = BasePrincipal::CreateContentPrincipal( + uri, BasePrincipal::Cast(aSelfOrigin)->OriginAttributesRef()); + if (NS_WARN_IF(!origin)) { + ReportToConsoleInvalidAllowValue(aDocument, curVal); + continue; + } + + feature.AppendToAllowList(origin); + } + } + + // No duplicate! + bool found = false; + for (const Feature& parsedFeature : parsedFeatures) { + if (parsedFeature.Name() == feature.Name()) { + found = true; + break; + } + } + + if (!found) { + parsedFeatures.AppendElement(feature); + } + } + + aParsedFeatures = std::move(parsedFeatures); + return true; +} + +} // namespace mozilla::dom diff --git a/dom/security/featurepolicy/FeaturePolicyParser.h b/dom/security/featurepolicy/FeaturePolicyParser.h new file mode 100644 index 0000000000..a60a391a78 --- /dev/null +++ b/dom/security/featurepolicy/FeaturePolicyParser.h @@ -0,0 +1,30 @@ +/* -*- 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_FeaturePolicyParser_h +#define mozilla_dom_FeaturePolicyParser_h + +#include "nsString.h" + +class nsIPrincipal; + +namespace mozilla::dom { + +class Document; +class Feature; + +class FeaturePolicyParser final { + public: + // aSelfOrigin must not be null. if aSrcOrigin is null, the parsing will not + // support 'src' as valid allow directive value. + static bool ParseString(const nsAString& aPolicy, Document* aDocument, + nsIPrincipal* aSelfOrigin, nsIPrincipal* aSrcOrigin, + nsTArray<Feature>& aParsedFeatures); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FeaturePolicyParser_h diff --git a/dom/security/featurepolicy/FeaturePolicyUtils.cpp b/dom/security/featurepolicy/FeaturePolicyUtils.cpp new file mode 100644 index 0000000000..1b2a383642 --- /dev/null +++ b/dom/security/featurepolicy/FeaturePolicyUtils.cpp @@ -0,0 +1,309 @@ +/* -*- 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 "FeaturePolicyUtils.h" +#include "nsIOService.h" + +#include "mozilla/dom/DOMTypes.h" +#include "mozilla/ipc/IPDLParamTraits.h" +#include "mozilla/dom/FeaturePolicyViolationReportBody.h" +#include "mozilla/dom/ReportingUtils.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/Document.h" +#include "nsContentUtils.h" +#include "nsJSUtils.h" + +namespace mozilla { +namespace dom { + +struct FeatureMap { + const char* mFeatureName; + FeaturePolicyUtils::FeaturePolicyValue mDefaultAllowList; +}; + +/* + * IMPORTANT: Do not change this list without review from a DOM peer _AND_ a + * DOM Security peer! + */ +static FeatureMap sSupportedFeatures[] = { + {"camera", FeaturePolicyUtils::FeaturePolicyValue::eSelf}, + {"geolocation", FeaturePolicyUtils::FeaturePolicyValue::eSelf}, + {"microphone", FeaturePolicyUtils::FeaturePolicyValue::eSelf}, + {"display-capture", FeaturePolicyUtils::FeaturePolicyValue::eSelf}, + {"fullscreen", FeaturePolicyUtils::FeaturePolicyValue::eSelf}, + {"web-share", FeaturePolicyUtils::FeaturePolicyValue::eSelf}, + {"gamepad", FeaturePolicyUtils::FeaturePolicyValue::eAll}, + {"speaker-selection", FeaturePolicyUtils::FeaturePolicyValue::eSelf}, +}; + +/* + * This is experimental features list, which is disabled by default by pref + * dom.security.featurePolicy.experimental.enabled. + */ +static FeatureMap sExperimentalFeatures[] = { + // We don't support 'autoplay' for now, because it would be overwrote by + // 'user-gesture-activation' policy. However, we can still keep it in the + // list as we might start supporting it after we use different autoplay + // policy. + {"autoplay", FeaturePolicyUtils::FeaturePolicyValue::eAll}, + {"encrypted-media", FeaturePolicyUtils::FeaturePolicyValue::eAll}, + {"midi", FeaturePolicyUtils::FeaturePolicyValue::eSelf}, + {"payment", FeaturePolicyUtils::FeaturePolicyValue::eAll}, + {"document-domain", FeaturePolicyUtils::FeaturePolicyValue::eAll}, + {"vr", FeaturePolicyUtils::FeaturePolicyValue::eAll}, + // https://immersive-web.github.io/webxr/#feature-policy + {"xr-spatial-tracking", FeaturePolicyUtils::FeaturePolicyValue::eSelf}, +}; + +/* static */ +bool FeaturePolicyUtils::IsExperimentalFeature(const nsAString& aFeatureName) { + uint32_t numFeatures = + (sizeof(sExperimentalFeatures) / sizeof(sExperimentalFeatures[0])); + for (uint32_t i = 0; i < numFeatures; ++i) { + if (aFeatureName.LowerCaseEqualsASCII( + sExperimentalFeatures[i].mFeatureName)) { + return true; + } + } + + return false; +} + +/* static */ +bool FeaturePolicyUtils::IsSupportedFeature(const nsAString& aFeatureName) { + uint32_t numFeatures = + (sizeof(sSupportedFeatures) / sizeof(sSupportedFeatures[0])); + for (uint32_t i = 0; i < numFeatures; ++i) { + if (aFeatureName.LowerCaseEqualsASCII(sSupportedFeatures[i].mFeatureName)) { + return true; + } + } + + return StaticPrefs::dom_security_featurePolicy_experimental_enabled() && + IsExperimentalFeature(aFeatureName); +} + +/* static */ +void FeaturePolicyUtils::ForEachFeature( + const std::function<void(const char*)>& aCallback) { + uint32_t numFeatures = + (sizeof(sSupportedFeatures) / sizeof(sSupportedFeatures[0])); + for (uint32_t i = 0; i < numFeatures; ++i) { + aCallback(sSupportedFeatures[i].mFeatureName); + } + + if (StaticPrefs::dom_security_featurePolicy_experimental_enabled()) { + numFeatures = + (sizeof(sExperimentalFeatures) / sizeof(sExperimentalFeatures[0])); + for (uint32_t i = 0; i < numFeatures; ++i) { + aCallback(sExperimentalFeatures[i].mFeatureName); + } + } +} + +/* static */ FeaturePolicyUtils::FeaturePolicyValue +FeaturePolicyUtils::DefaultAllowListFeature(const nsAString& aFeatureName) { + uint32_t numFeatures = + (sizeof(sSupportedFeatures) / sizeof(sSupportedFeatures[0])); + for (uint32_t i = 0; i < numFeatures; ++i) { + if (aFeatureName.LowerCaseEqualsASCII(sSupportedFeatures[i].mFeatureName)) { + return sSupportedFeatures[i].mDefaultAllowList; + } + } + + if (StaticPrefs::dom_security_featurePolicy_experimental_enabled()) { + numFeatures = + (sizeof(sExperimentalFeatures) / sizeof(sExperimentalFeatures[0])); + for (uint32_t i = 0; i < numFeatures; ++i) { + if (aFeatureName.LowerCaseEqualsASCII( + sExperimentalFeatures[i].mFeatureName)) { + return sExperimentalFeatures[i].mDefaultAllowList; + } + } + } + + return FeaturePolicyValue::eNone; +} + +static bool IsSameOriginAsTop(Document* aDocument) { + MOZ_ASSERT(aDocument); + + BrowsingContext* browsingContext = aDocument->GetBrowsingContext(); + if (!browsingContext) { + return false; + } + + nsPIDOMWindowOuter* topWindow = browsingContext->Top()->GetDOMWindow(); + if (!topWindow) { + // If we don't have a DOMWindow, We are not in same origin. + return false; + } + + Document* topLevelDocument = topWindow->GetExtantDoc(); + if (!topLevelDocument) { + return false; + } + + return NS_SUCCEEDED( + nsContentUtils::CheckSameOrigin(topLevelDocument, aDocument)); +} + +/* static */ +bool FeaturePolicyUtils::IsFeatureUnsafeAllowedAll( + Document* aDocument, const nsAString& aFeatureName) { + MOZ_ASSERT(aDocument); + + if (!aDocument->IsHTMLDocument()) { + return false; + } + + FeaturePolicy* policy = aDocument->FeaturePolicy(); + MOZ_ASSERT(policy); + + return policy->HasFeatureUnsafeAllowsAll(aFeatureName) && + !policy->IsSameOriginAsSrc(aDocument->NodePrincipal()) && + !policy->AllowsFeatureExplicitlyInAncestorChain( + aFeatureName, policy->DefaultOrigin()) && + !IsSameOriginAsTop(aDocument); +} + +/* static */ +bool FeaturePolicyUtils::IsFeatureAllowed(Document* aDocument, + const nsAString& aFeatureName) { + MOZ_ASSERT(aDocument); + + // Skip apply features in experimental phase + if (!StaticPrefs::dom_security_featurePolicy_experimental_enabled() && + IsExperimentalFeature(aFeatureName)) { + return true; + } + + FeaturePolicy* policy = aDocument->FeaturePolicy(); + MOZ_ASSERT(policy); + + if (policy->AllowsFeatureInternal(aFeatureName, policy->DefaultOrigin())) { + return true; + } + + ReportViolation(aDocument, aFeatureName); + return false; +} + +/* static */ +void FeaturePolicyUtils::ReportViolation(Document* aDocument, + const nsAString& aFeatureName) { + MOZ_ASSERT(aDocument); + + nsCOMPtr<nsIURI> uri = aDocument->GetDocumentURI(); + if (NS_WARN_IF(!uri)) { + return; + } + + // Strip the URL of any possible username/password and make it ready to be + // presented in the UI. + nsCOMPtr<nsIURI> exposableURI = net::nsIOService::CreateExposableURI(uri); + nsAutoCString spec; + nsresult rv = exposableURI->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (NS_WARN_IF(!cx)) { + return; + } + + nsAutoString fileName; + Nullable<int32_t> lineNumber; + Nullable<int32_t> columnNumber; + uint32_t line = 0; + uint32_t column = 0; + if (nsJSUtils::GetCallingLocation(cx, fileName, &line, &column)) { + lineNumber.SetValue(static_cast<int32_t>(line)); + columnNumber.SetValue(static_cast<int32_t>(column)); + } + + nsPIDOMWindowInner* window = aDocument->GetInnerWindow(); + if (NS_WARN_IF(!window)) { + return; + } + + RefPtr<FeaturePolicyViolationReportBody> body = + new FeaturePolicyViolationReportBody(window->AsGlobal(), aFeatureName, + fileName, lineNumber, columnNumber, + u"enforce"_ns); + + ReportingUtils::Report(window->AsGlobal(), nsGkAtoms::featurePolicyViolation, + u"default"_ns, NS_ConvertUTF8toUTF16(spec), body); +} + +} // namespace dom + +namespace ipc { +void IPDLParamTraits<dom::FeaturePolicy*>::Write(IPC::MessageWriter* aWriter, + IProtocol* aActor, + dom::FeaturePolicy* aParam) { + if (!aParam) { + WriteIPDLParam(aWriter, aActor, false); + return; + } + + WriteIPDLParam(aWriter, aActor, true); + + dom::FeaturePolicyInfo info; + info.defaultOrigin() = aParam->DefaultOrigin(); + info.selfOrigin() = aParam->GetSelfOrigin(); + info.srcOrigin() = aParam->GetSrcOrigin(); + + info.declaredString() = aParam->DeclaredString(); + info.inheritedDeniedFeatureNames() = + aParam->InheritedDeniedFeatureNames().Clone(); + info.attributeEnabledFeatureNames() = + aParam->AttributeEnabledFeatureNames().Clone(); + + WriteIPDLParam(aWriter, aActor, info); +} + +bool IPDLParamTraits<dom::FeaturePolicy*>::Read( + IPC::MessageReader* aReader, IProtocol* aActor, + RefPtr<dom::FeaturePolicy>* aResult) { + *aResult = nullptr; + bool notnull = false; + if (!ReadIPDLParam(aReader, aActor, ¬null)) { + return false; + } + + if (!notnull) { + return true; + } + + dom::FeaturePolicyInfo info; + if (!ReadIPDLParam(aReader, aActor, &info)) { + return false; + } + + // Note that we only do IPC for feature policy to inherit policy from parent + // to child document. That does not need to bind feature policy with a node. + RefPtr<dom::FeaturePolicy> featurePolicy = new dom::FeaturePolicy(nullptr); + featurePolicy->SetDefaultOrigin(info.defaultOrigin()); + featurePolicy->SetInheritedDeniedFeatureNames( + info.inheritedDeniedFeatureNames()); + + const auto& declaredString = info.declaredString(); + if (info.selfOrigin() && !declaredString.IsEmpty()) { + featurePolicy->SetDeclaredPolicy(nullptr, declaredString, info.selfOrigin(), + info.srcOrigin()); + } + + for (auto& featureName : info.attributeEnabledFeatureNames()) { + featurePolicy->MaybeSetAllowedPolicy(featureName); + } + + *aResult = std::move(featurePolicy); + return true; +} +} // namespace ipc + +} // namespace mozilla diff --git a/dom/security/featurepolicy/FeaturePolicyUtils.h b/dom/security/featurepolicy/FeaturePolicyUtils.h new file mode 100644 index 0000000000..380806433d --- /dev/null +++ b/dom/security/featurepolicy/FeaturePolicyUtils.h @@ -0,0 +1,91 @@ +/* -*- 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_FeaturePolicyUtils_h +#define mozilla_dom_FeaturePolicyUtils_h + +#include "nsString.h" +#include <functional> + +#include "mozilla/dom/FeaturePolicy.h" + +class PickleIterator; + +namespace IPC { +class Message; +class MessageReader; +class MessageWriter; +} // namespace IPC + +namespace mozilla { +namespace dom { + +class Document; + +class FeaturePolicyUtils final { + public: + enum FeaturePolicyValue { + // Feature always allowed. + eAll, + + // Feature allowed for documents that are same-origin with this one. + eSelf, + + // Feature denied. + eNone, + }; + + // This method returns true if aFeatureName is allowed for aDocument. + // Use this method everywhere you need to check feature-policy directives. + static bool IsFeatureAllowed(Document* aDocument, + const nsAString& aFeatureName); + + // Returns true if aFeatureName is a known feature policy name. + static bool IsSupportedFeature(const nsAString& aFeatureName); + + // Returns true if aFeatureName is a experimental feature policy name. + static bool IsExperimentalFeature(const nsAString& aFeatureName); + + // Runs aCallback for each known feature policy, with the feature name as + // argument. + static void ForEachFeature(const std::function<void(const char*)>& aCallback); + + // Returns the default policy value for aFeatureName. + static FeaturePolicyValue DefaultAllowListFeature( + const nsAString& aFeatureName); + + // This method returns true if aFeatureName is in unsafe allowed "*" case. + // We are in "unsafe" case when there is 'allow "*"' presents for an origin + // that's not presented in the ancestor feature policy chain, via src, via + // explicitly listed in allow, and not being the top-level origin. + static bool IsFeatureUnsafeAllowedAll(Document* aDocument, + const nsAString& aFeatureName); + + private: + static void ReportViolation(Document* aDocument, + const nsAString& aFeatureName); +}; + +} // namespace dom + +namespace ipc { + +class IProtocol; + +template <typename T> +struct IPDLParamTraits; + +template <> +struct IPDLParamTraits<mozilla::dom::FeaturePolicy*> { + static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor, + mozilla::dom::FeaturePolicy* aParam); + static bool Read(IPC::MessageReader* aReader, IProtocol* aActor, + RefPtr<mozilla::dom::FeaturePolicy>* aResult); +}; +} // namespace ipc +} // namespace mozilla + +#endif // mozilla_dom_FeaturePolicyUtils_h diff --git a/dom/security/featurepolicy/fuzztest/fp_fuzzer.cpp b/dom/security/featurepolicy/fuzztest/fp_fuzzer.cpp new file mode 100644 index 0000000000..25f7dc8d41 --- /dev/null +++ b/dom/security/featurepolicy/fuzztest/fp_fuzzer.cpp @@ -0,0 +1,67 @@ +/* -*- 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 https://mozilla.org/MPL/2.0/. */ + +#include "FuzzingInterface.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/Feature.h" +#include "mozilla/dom/FeaturePolicyParser.h" +#include "nsNetUtil.h" +#include "nsStringFwd.h" +#include "nsTArray.h" + +using namespace mozilla; +using namespace mozilla::dom; + +static nsCOMPtr<nsIPrincipal> selfURIPrincipal; +static nsCOMPtr<nsIURI> selfURI; + +static int LVVMFuzzerInitTest(int* argc, char*** argv) { + nsresult ret; + ret = NS_NewURI(getter_AddRefs(selfURI), "http://selfuri.com"); + if (ret != NS_OK) { + MOZ_CRASH("NS_NewURI failed."); + } + + mozilla::OriginAttributes attrs; + selfURIPrincipal = + mozilla::BasePrincipal::CreateContentPrincipal(selfURI, attrs); + if (!selfURIPrincipal) { + MOZ_CRASH("CreateContentPrincipal failed."); + } + return 0; +} + +static int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + if (!size) { + return 0; + } + nsTArray<Feature> parsedFeatures; + + NS_ConvertASCIItoUTF16 policy(reinterpret_cast<const char*>(data), size); + if (!policy.get()) return 0; + + FeaturePolicyParser::ParseString(policy, nullptr, selfURIPrincipal, + selfURIPrincipal, parsedFeatures); + + for (const Feature& feature : parsedFeatures) { + nsTArray<nsCOMPtr<nsIPrincipal>> list; + feature.GetAllowList(list); + + for (nsIPrincipal* principal : list) { + nsAutoCString originNoSuffix; + nsresult rv = principal->GetOriginNoSuffix(originNoSuffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return 0; + } + printf("%s - %s\n", NS_ConvertUTF16toUTF8(feature.Name()).get(), + originNoSuffix.get()); + } + } + return 0; +} + +MOZ_FUZZING_INTERFACE_RAW(LVVMFuzzerInitTest, LLVMFuzzerTestOneInput, + FeaturePolicyParser); diff --git a/dom/security/featurepolicy/fuzztest/fp_fuzzer.dict b/dom/security/featurepolicy/fuzztest/fp_fuzzer.dict new file mode 100644 index 0000000000..e95508bf8e --- /dev/null +++ b/dom/security/featurepolicy/fuzztest/fp_fuzzer.dict @@ -0,0 +1,54 @@ +# tokens +"'" +";" + +### https://www.w3.org/TR/{CSP,CSP2,CSP3}/ +# directive names +"accelerometer" +"ambient-light-sensor" +"autoplay" +"battery" +"camera" +"display-capture" +"document-domain" +"encrypted-media" +"execution-while-not-rendered" +"execution-while-out-of-viewport" +"fullscreen +"geolocation +"gyroscope" +"layout-animations" +"legacy-image-formats" +"magnetometer" +"microphone" +"midi" +"navigation-override" +"oversized-images" +"payment" +"picture-in-picture" +"publickey-credentials" +"sync-xhr" +"usb" +"vr" +"wake-lock" +"xr-spatial-tracking" + +# directive values +"'self'" +"'none'" +"'src''" +* + + +# URI components +"https:" +"ws:" +"blob:" +"data:" +"filesystem:" +"javascript:" +"http://" +"selfuri.com" +"127.0.0.1" +"::1" +https://example.com
\ No newline at end of file diff --git a/dom/security/featurepolicy/fuzztest/moz.build b/dom/security/featurepolicy/fuzztest/moz.build new file mode 100644 index 0000000000..ea577e8339 --- /dev/null +++ b/dom/security/featurepolicy/fuzztest/moz.build @@ -0,0 +1,18 @@ +# -*- 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/. + +Library("FuzzingFeaturePolicy") + +LOCAL_INCLUDES += [ + "/dom/security/featurepolicy", + "/netwerk/base", +] + +include("/tools/fuzzing/libfuzzer-config.mozbuild") + +SOURCES += ["fp_fuzzer.cpp"] + +FINAL_LIBRARY = "xul-gtest" diff --git a/dom/security/featurepolicy/moz.build b/dom/security/featurepolicy/moz.build new file mode 100644 index 0000000000..40277836ad --- /dev/null +++ b/dom/security/featurepolicy/moz.build @@ -0,0 +1,36 @@ +# -*- 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Security") + +TEST_DIRS += ["test/gtest"] +MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.ini"] + +EXPORTS.mozilla.dom += [ + "Feature.h", + "FeaturePolicy.h", + "FeaturePolicyParser.h", + "FeaturePolicyUtils.h", +] + +UNIFIED_SOURCES += [ + "Feature.cpp", + "FeaturePolicy.cpp", + "FeaturePolicyParser.cpp", + "FeaturePolicyUtils.cpp", +] + +LOCAL_INCLUDES += [ + "/netwerk/base", +] +include("/ipc/chromium/chromium-config.mozbuild") +include("/tools/fuzzing/libfuzzer-config.mozbuild") + +FINAL_LIBRARY = "xul" + +if CONFIG["FUZZING_INTERFACES"]: + TEST_DIRS += ["fuzztest"] diff --git a/dom/security/featurepolicy/test/gtest/TestFeaturePolicyParser.cpp b/dom/security/featurepolicy/test/gtest/TestFeaturePolicyParser.cpp new file mode 100644 index 0000000000..3e58971c9b --- /dev/null +++ b/dom/security/featurepolicy/test/gtest/TestFeaturePolicyParser.cpp @@ -0,0 +1,162 @@ +/* -*- 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 "mozilla/BasePrincipal.h" +#include "mozilla/dom/Feature.h" +#include "mozilla/dom/FeaturePolicyParser.h" +#include "nsNetUtil.h" +#include "nsTArray.h" + +using namespace mozilla; +using namespace mozilla::dom; + +#define URL_SELF "https://example.com"_ns +#define URL_EXAMPLE_COM "http://example.com"_ns +#define URL_EXAMPLE_NET "http://example.net"_ns + +void CheckParser(const nsAString& aInput, bool aExpectedResults, + uint32_t aExpectedFeatures, + nsTArray<Feature>& aParsedFeatures) { + nsCOMPtr<nsIPrincipal> principal = + mozilla::BasePrincipal::CreateContentPrincipal(URL_SELF); + nsTArray<Feature> parsedFeatures; + ASSERT_TRUE(FeaturePolicyParser::ParseString(aInput, nullptr, principal, + principal, parsedFeatures) == + aExpectedResults); + ASSERT_TRUE(parsedFeatures.Length() == aExpectedFeatures); + + aParsedFeatures = std::move(parsedFeatures); +} + +TEST(FeaturePolicyParser, Basic) +{ + nsCOMPtr<nsIPrincipal> selfPrincipal = + mozilla::BasePrincipal::CreateContentPrincipal(URL_SELF); + nsCOMPtr<nsIPrincipal> exampleComPrincipal = + mozilla::BasePrincipal::CreateContentPrincipal(URL_EXAMPLE_COM); + nsCOMPtr<nsIPrincipal> exampleNetPrincipal = + mozilla::BasePrincipal::CreateContentPrincipal(URL_EXAMPLE_NET); + + nsTArray<Feature> parsedFeatures; + + // Empty string is a valid policy. + CheckParser(u""_ns, true, 0, parsedFeatures); + + // Empty string with spaces is still valid. + CheckParser(u" "_ns, true, 0, parsedFeatures); + + // Non-Existing features with no allowed values + CheckParser(u"non-existing-feature"_ns, true, 0, parsedFeatures); + CheckParser(u"non-existing-feature;another-feature"_ns, true, 0, + parsedFeatures); + + // Existing feature with no allowed values + CheckParser(u"camera"_ns, true, 1, parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].HasAllowList()); + + // Some spaces. + CheckParser(u" camera "_ns, true, 1, parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].HasAllowList()); + + // A random ; + CheckParser(u"camera;"_ns, true, 1, parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].HasAllowList()); + + // Another random ; + CheckParser(u";camera;"_ns, true, 1, parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].HasAllowList()); + + // 2 features + CheckParser(u"camera;microphone"_ns, true, 2, parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].HasAllowList()); + ASSERT_TRUE(parsedFeatures[1].Name().Equals(u"microphone"_ns)); + ASSERT_TRUE(parsedFeatures[1].HasAllowList()); + + // 2 features with spaces + CheckParser(u" camera ; microphone "_ns, true, 2, parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].HasAllowList()); + ASSERT_TRUE(parsedFeatures[1].Name().Equals(u"microphone"_ns)); + ASSERT_TRUE(parsedFeatures[1].HasAllowList()); + + // 3 features, but only 2 exist. + CheckParser(u"camera;microphone;foobar"_ns, true, 2, parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].HasAllowList()); + ASSERT_TRUE(parsedFeatures[1].Name().Equals(u"microphone"_ns)); + ASSERT_TRUE(parsedFeatures[1].HasAllowList()); + + // Multiple spaces around the value + CheckParser(u"camera 'self'"_ns, true, 1, parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].AllowListContains(selfPrincipal)); + + // Multiple spaces around the value + CheckParser(u"camera 'self' "_ns, true, 1, parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].AllowListContains(selfPrincipal)); + + // No final ' + CheckParser(u"camera 'self"_ns, true, 1, parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].HasAllowList()); + ASSERT_TRUE(!parsedFeatures[0].AllowListContains(selfPrincipal)); + + // Lowercase/Uppercase + CheckParser(u"camera 'selF'"_ns, true, 1, parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].AllowListContains(selfPrincipal)); + + // Lowercase/Uppercase + CheckParser(u"camera * 'self' none' a.com 123"_ns, true, 1, parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].AllowsAll()); + + // After a 'none' we don't continue the parsing. + CheckParser(u"camera 'none' a.com b.org c.net d.co.uk"_ns, true, 1, + parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].AllowsNone()); + + // After a * we don't continue the parsing. + CheckParser(u"camera * a.com b.org c.net d.co.uk"_ns, true, 1, + parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].AllowsAll()); + + // 'self' + CheckParser(u"camera 'self'"_ns, true, 1, parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].AllowListContains(selfPrincipal)); + + // A couple of URLs + CheckParser(u"camera http://example.com http://example.net"_ns, true, 1, + parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(!parsedFeatures[0].AllowListContains(selfPrincipal)); + ASSERT_TRUE(parsedFeatures[0].AllowListContains(exampleComPrincipal)); + ASSERT_TRUE(parsedFeatures[0].AllowListContains(exampleNetPrincipal)); + + // A couple of URLs + self + CheckParser(u"camera http://example.com 'self' http://example.net"_ns, true, + 1, parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].AllowListContains(selfPrincipal)); + ASSERT_TRUE(parsedFeatures[0].AllowListContains(exampleComPrincipal)); + ASSERT_TRUE(parsedFeatures[0].AllowListContains(exampleNetPrincipal)); + + // A couple of URLs but then * + CheckParser(u"camera http://example.com 'self' http://example.net *"_ns, true, + 1, parsedFeatures); + ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns)); + ASSERT_TRUE(parsedFeatures[0].AllowsAll()); +} diff --git a/dom/security/featurepolicy/test/gtest/moz.build b/dom/security/featurepolicy/test/gtest/moz.build new file mode 100644 index 0000000000..e307810ff2 --- /dev/null +++ b/dom/security/featurepolicy/test/gtest/moz.build @@ -0,0 +1,13 @@ +# -*- 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 = [ + "TestFeaturePolicyParser.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" diff --git a/dom/security/featurepolicy/test/mochitest/empty.html b/dom/security/featurepolicy/test/mochitest/empty.html new file mode 100644 index 0000000000..64355e7d19 --- /dev/null +++ b/dom/security/featurepolicy/test/mochitest/empty.html @@ -0,0 +1 @@ +Nothing here diff --git a/dom/security/featurepolicy/test/mochitest/mochitest.ini b/dom/security/featurepolicy/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..3c1850cc2e --- /dev/null +++ b/dom/security/featurepolicy/test/mochitest/mochitest.ini @@ -0,0 +1,11 @@ +[DEFAULT] +prefs = + dom.security.featurePolicy.header.enabled=true + dom.security.featurePolicy.webidl.enabled=true +support-files = + empty.html + test_parser.html^headers^ + +[test_parser.html] +fail-if = xorigin +[test_featureList.html] diff --git a/dom/security/featurepolicy/test/mochitest/test_featureList.html b/dom/security/featurepolicy/test/mochitest/test_featureList.html new file mode 100644 index 0000000000..377c5fccb4 --- /dev/null +++ b/dom/security/featurepolicy/test/mochitest/test_featureList.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test feature policy - list</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 src="empty.html" id="ifr"></iframe> +<script type="text/javascript"> + +let supportedFeatures = [ + "autoplay", + "camera", + "encrypted-media", + "fullscreen", + "gamepad", + "geolocation", + "microphone", + "midi", + "payment", + "display-capture", + "document-domain", + "speaker-selection", + "vr", + "web-share", +]; + +function checkFeatures(features) { + features.forEach(feature => { + ok(supportedFeatures.includes(feature), "Feature: " + feature); + }); +} + +ok("featurePolicy" in document, "We have document.featurePolicy"); +checkFeatures(document.featurePolicy.features()); + +let ifr = document.getElementById("ifr"); +ok("featurePolicy" in ifr, "We have HTMLIFrameElement.featurePolicy"); +checkFeatures(ifr.featurePolicy.features()); + +</script> +</body> +</html> diff --git a/dom/security/featurepolicy/test/mochitest/test_parser.html b/dom/security/featurepolicy/test/mochitest/test_parser.html new file mode 100644 index 0000000000..a8322f6e7d --- /dev/null +++ b/dom/security/featurepolicy/test/mochitest/test_parser.html @@ -0,0 +1,418 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test feature policy - parsing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe src="empty.html" id="ifr"></iframe> +<iframe src="https://example.org/tests/dom/security/featurePolicy/test/mochitest/empty.html" id="cross_ifr"></iframe> +<script type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +const CROSS_ORIGIN = "https://example.org"; + +function test_document() { + info("Checking document.featurePolicy"); + ok("featurePolicy" in document, "We have document.featurePolicy"); + + ok(!document.featurePolicy.allowsFeature("foobar"), "Random feature"); + ok(!document.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature"); + + ok(document.featurePolicy.allowsFeature("camera"), "Camera is allowed for self"); + ok(document.featurePolicy.allowsFeature("camera", "https://foo.bar"), "Camera is always allowed"); + let allowed = document.featurePolicy.getAllowlistForFeature("camera"); + is(allowed.length, 1, "Only 1 entry in allowlist for camera"); + is(allowed[0], "*", "allowlist is *"); + + ok(document.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self"); + ok(document.featurePolicy.allowsFeature("geolocation", location.origin), "Geolocation is allowed for self"); + ok(!document.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL"); + allowed = document.featurePolicy.getAllowlistForFeature("geolocation"); + is(allowed.length, 1, "Only 1 entry in allowlist for geolocation"); + is(allowed[0], location.origin, "allowlist is self"); + + ok(!document.featurePolicy.allowsFeature("microphone"), "Microphone is disabled for self"); + ok(!document.featurePolicy.allowsFeature("microphone", location.origin), "Microphone is disabled for self"); + ok(!document.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar"); + ok(document.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is allowed for example.com"); + ok(document.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is allowed for example.org"); + allowed = document.featurePolicy.getAllowlistForFeature("microphone"); + is(allowed.length, 0, "No allowlist for microphone"); + + ok(!document.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self"); + ok(!document.featurePolicy.allowsFeature("vr", location.origin), "Vibrate is disabled for self"); + ok(!document.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar"); + allowed = document.featurePolicy.getAllowlistForFeature("vr"); + is(allowed.length, 0, "No allowlist for vr"); + + allowed = document.featurePolicy.allowedFeatures(); + // microphone is disabled for this origin, vr is disabled everywhere. + let camera = false; + let geolocation = false; + allowed.forEach(a => { + if (a == "camera") camera = true; + if (a == "geolocation") geolocation = true; + }); + + ok(camera, "Camera is always allowed"); + ok(geolocation, "Geolocation is allowed only for self"); + + next(); +} + +function test_iframe_without_allow() { + info("Checking HTMLIFrameElement.featurePolicy"); + let ifr = document.getElementById("ifr"); + ok("featurePolicy" in ifr, "HTMLIFrameElement.featurePolicy exists"); + + ok(!ifr.featurePolicy.allowsFeature("foobar"), "Random feature"); + ok(!ifr.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature"); + + ok(ifr.featurePolicy.allowsFeature("camera"), "Camera is allowed for self"); + ok(ifr.featurePolicy.allowsFeature("camera", location.origin), "Camera is allowed for self"); + ok(!ifr.featurePolicy.allowsFeature("camera", "https://foo.bar"), "Camera is not allowed for a random URL"); + let allowed = ifr.featurePolicy.getAllowlistForFeature("camera"); + is(allowed.length, 1, "Only 1 entry in allowlist for camera"); + is(allowed[0], location.origin, "allowlist is 'self'"); + + ok(ifr.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self"); + ok(ifr.featurePolicy.allowsFeature("geolocation", location.origin), "Geolocation is allowed for self"); + ok(!ifr.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL"); + allowed = ifr.featurePolicy.getAllowlistForFeature("geolocation"); + is(allowed.length, 1, "Only 1 entry in allowlist for geolocation"); + is(allowed[0], location.origin, "allowlist is '*'"); + + ok(!ifr.featurePolicy.allowsFeature("microphone"), "Microphone is disabled for self"); + ok(!ifr.featurePolicy.allowsFeature("microphone", location.origin), "Microphone is disabled for self"); + ok(!ifr.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar"); + ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com"); + ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is disabled for example.org"); + allowed = ifr.featurePolicy.getAllowlistForFeature("microphone"); + is(allowed.length, 0, "No allowlist for microphone"); + + ok(!ifr.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self"); + ok(!ifr.featurePolicy.allowsFeature("vr", location.origin), "Vibrate is disabled for self"); + ok(!ifr.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar"); + allowed = ifr.featurePolicy.getAllowlistForFeature("vr"); + is(allowed.length, 0, "No allowlist for vr"); + + ok(ifr.featurePolicy.allowedFeatures().includes("camera"), "Camera is allowed"); + ok(ifr.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is allowed"); + // microphone is disabled for this origin + ok(!ifr.featurePolicy.allowedFeatures().includes("microphone"), "microphone is not allowed"); + // vr is disabled everywhere. + ok(!ifr.featurePolicy.allowedFeatures().includes("vr"), "VR is not allowed"); + + next(); +} + +function test_iframe_with_allow() { + info("Checking HTMLIFrameElement.featurePolicy"); + let ifr = document.getElementById("ifr"); + ok("featurePolicy" in ifr, "HTMLIFrameElement.featurePolicy exists"); + + ifr.setAttribute("allow", "camera 'none'"); + + ok(!ifr.featurePolicy.allowsFeature("foobar"), "Random feature"); + ok(!ifr.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature"); + + ok(!ifr.featurePolicy.allowsFeature("camera"), "Camera is not allowed"); + let allowed = ifr.featurePolicy.getAllowlistForFeature("camera"); + is(allowed.length, 0, "Camera has an empty allowlist"); + + ok(ifr.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self"); + ok(ifr.featurePolicy.allowsFeature("geolocation", location.origin), "Geolocation is allowed for self"); + ok(!ifr.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL"); + allowed = ifr.featurePolicy.getAllowlistForFeature("geolocation"); + is(allowed.length, 1, "Only 1 entry in allowlist for geolocation"); + is(allowed[0], location.origin, "allowlist is '*'"); + + ok(!ifr.featurePolicy.allowsFeature("microphone"), "Microphone is disabled for self"); + ok(!ifr.featurePolicy.allowsFeature("microphone", location.origin), "Microphone is disabled for self"); + ok(!ifr.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar"); + ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com"); + ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is disabled for example.org"); + allowed = ifr.featurePolicy.getAllowlistForFeature("microphone"); + is(allowed.length, 0, "No allowlist for microphone"); + + ok(!ifr.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self"); + ok(!ifr.featurePolicy.allowsFeature("vr", location.origin), "Vibrate is disabled for self"); + ok(!ifr.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar"); + allowed = ifr.featurePolicy.getAllowlistForFeature("vr"); + is(allowed.length, 0, "No allowlist for vr"); + + ok(ifr.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is allowed only for self"); + + next(); +} + +function test_iframe_contentDocument() { + info("Checking iframe document.featurePolicy"); + + let ifr = document.createElement("iframe"); + ifr.setAttribute("src", "empty.html"); + ifr.onload = function() { + ok("featurePolicy" in ifr.contentDocument, "We have ifr.contentDocument.featurePolicy"); + + ok(!ifr.contentDocument.featurePolicy.allowsFeature("foobar"), "Random feature"); + ok(!ifr.contentDocument.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature"); + + ok(ifr.contentDocument.featurePolicy.allowsFeature("camera"), "Camera is allowed for self"); + ok(ifr.contentDocument.featurePolicy.allowsFeature("camera", location.origin), "Camera is allowed for self"); + ok(!ifr.contentDocument.featurePolicy.allowsFeature("camera", "https://foo.bar"), "Camera is allowed for self"); + let allowed = ifr.contentDocument.featurePolicy.getAllowlistForFeature("camera"); + is(allowed.length, 1, "Only 1 entry in allowlist for camera"); + is(allowed[0], location.origin, "allowlist is 'self'"); + + ok(ifr.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self"); + ok(ifr.featurePolicy.allowsFeature("geolocation", location.origin), "Geolocation is allowed for self"); + ok(!ifr.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL"); + allowed = ifr.featurePolicy.getAllowlistForFeature("geolocation"); + is(allowed.length, 1, "Only 1 entry in allowlist for geolocation"); + is(allowed[0], location.origin, "allowlist is '*'"); + + ok(!ifr.contentDocument.featurePolicy.allowsFeature("microphone"), "Microphone is disabled for self"); + ok(!ifr.contentDocument.featurePolicy.allowsFeature("microphone", location.origin), "Microphone is disabled for self"); + ok(!ifr.contentDocument.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar"); + ok(!ifr.contentDocument.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is allowed for example.com"); + ok(!ifr.contentDocument.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is allowed for example.org"); + allowed = ifr.contentDocument.featurePolicy.getAllowlistForFeature("microphone"); + is(allowed.length, 0, "No allowlist for microphone"); + + ok(!ifr.contentDocument.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self"); + ok(!ifr.contentDocument.featurePolicy.allowsFeature("vr", location.origin), "Vibrate is disabled for self"); + ok(!ifr.contentDocument.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar"); + allowed = ifr.contentDocument.featurePolicy.getAllowlistForFeature("vr"); + is(allowed.length, 0, "No allowlist for vr"); + + ok(ifr.contentDocument.featurePolicy.allowedFeatures().includes("camera"), "Camera is allowed"); + ok(ifr.contentDocument.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is allowed"); + // microphone is disabled for this origin + ok(!ifr.contentDocument.featurePolicy.allowedFeatures().includes("microphone"), "Microphone is not allowed"); + // vr is disabled everywhere. + ok(!ifr.contentDocument.featurePolicy.allowedFeatures().includes("vr"), "VR is not allowed"); + + next(); + }; + document.body.appendChild(ifr); +} + +function test_cross_iframe_without_allow() { + info("Checking cross HTMLIFrameElement.featurePolicy no allow"); + let ifr = document.getElementById("cross_ifr"); + ok("featurePolicy" in ifr, "HTMLIFrameElement.featurePolicy exists"); + + ok(!ifr.featurePolicy.allowsFeature("foobar"), "Random feature"); + ok(!ifr.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature"); + + ok(ifr.featurePolicy.allowsFeature("camera"), "Camera is allowed for self"); + ok(ifr.featurePolicy.allowsFeature("camera", CROSS_ORIGIN), "Camera is allowed for self"); + ok(!ifr.featurePolicy.allowsFeature("camera", "https://foo.bar"), "Camera is not allowed for a random URL"); + let allowed = ifr.featurePolicy.getAllowlistForFeature("camera"); + is(allowed.length, 1, "Only 1 entry in allowlist for camera"); + is(allowed[0], CROSS_ORIGIN, "allowlist is 'self'"); + + ok(!ifr.featurePolicy.allowsFeature("geolocation"), "Geolocation is not allowed for self"); + ok(!ifr.featurePolicy.allowsFeature("geolocation", CROSS_ORIGIN), + "Geolocation is not allowed for self"); + ok(!ifr.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL"); + allowed = ifr.featurePolicy.getAllowlistForFeature("geolocation"); + is(allowed.length, 0, "No allowlist for geolocation"); + + ok(ifr.featurePolicy.allowsFeature("microphone"), "Microphone is enabled for self"); + ok(ifr.featurePolicy.allowsFeature("microphone", CROSS_ORIGIN), "Microphone is enabled for self"); + ok(!ifr.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar"); + ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com"); + allowed = ifr.featurePolicy.getAllowlistForFeature("microphone"); + is(allowed.length, 1, "Only 1 entry in allowlist for microphone"); + is(allowed[0], CROSS_ORIGIN, "allowlist is self"); + + ok(!ifr.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self"); + ok(!ifr.featurePolicy.allowsFeature("vr", CROSS_ORIGIN), "Vibrate is disabled for self"); + ok(!ifr.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar"); + allowed = ifr.featurePolicy.getAllowlistForFeature("vr"); + is(allowed.length, 0, "No allowlist for vr"); + + ok(ifr.featurePolicy.allowedFeatures().includes("camera"), "Camera is allowed"); + ok(!ifr.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is not allowed"); + // microphone is enabled for this origin + ok(ifr.featurePolicy.allowedFeatures().includes("microphone"), "microphone is allowed"); + // vr is disabled everywhere. + ok(!ifr.featurePolicy.allowedFeatures().includes("vr"), "VR is not allowed"); + + next(); +} + +function test_cross_iframe_with_allow() { + info("Checking cross HTMLIFrameElement.featurePolicy with allow"); + let ifr = document.getElementById("cross_ifr"); + ok("featurePolicy" in ifr, "HTMLIFrameElement.featurePolicy exists"); + + ifr.setAttribute("allow", "geolocation; camera 'none'"); + + ok(!ifr.featurePolicy.allowsFeature("foobar"), "Random feature"); + ok(!ifr.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature"); + + ok(!ifr.featurePolicy.allowsFeature("camera"), "Camera is not allowed"); + let allowed = ifr.featurePolicy.getAllowlistForFeature("camera"); + is(allowed.length, 0, "Camera has an empty allowlist"); + + ok(ifr.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self"); + ok(ifr.featurePolicy.allowsFeature("geolocation", CROSS_ORIGIN), "Geolocation is allowed for self"); + ok(!ifr.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL"); + allowed = ifr.featurePolicy.getAllowlistForFeature("geolocation"); + is(allowed.length, 1, "Only 1 entry in allowlist for geolocation"); + is(allowed[0], CROSS_ORIGIN, "allowlist is '*'"); + + ok(ifr.featurePolicy.allowsFeature("microphone"), "Microphone is enabled for self"); + ok(ifr.featurePolicy.allowsFeature("microphone", CROSS_ORIGIN), "Microphone is enabled for self"); + ok(!ifr.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar"); + ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com"); + allowed = ifr.featurePolicy.getAllowlistForFeature("microphone"); + is(allowed.length, 1, "Only 1 entry in allowlist for microphone"); + is(allowed[0], CROSS_ORIGIN, "allowlist is self"); + + ok(!ifr.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self"); + ok(!ifr.featurePolicy.allowsFeature("vr", CROSS_ORIGIN), "Vibrate is disabled for self"); + ok(!ifr.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar"); + allowed = ifr.featurePolicy.getAllowlistForFeature("vr"); + is(allowed.length, 0, "No allowlist for vr"); + + ok(ifr.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is allowed only for self"); + // microphone is enabled for this origin + ok(ifr.featurePolicy.allowedFeatures().includes("microphone"), "microphone is allowed"); + + next(); +} + +function test_cross_iframe_contentDocument_no_allow() { + info("Checking cross iframe document.featurePolicy no allow"); + + let ifr = document.createElement("iframe"); + ifr.setAttribute("src", "https://example.org/tests/dom/security/featurePolicy/test/mochitest/empty.html"); + ifr.onload = async function() { + await SpecialPowers.spawn(ifr, [], () => { + Assert.ok("featurePolicy" in this.content.document, "We have this.content.document.featurePolicy"); + + Assert.ok(!this.content.document.featurePolicy.allowsFeature("foobar"), "Random feature"); + Assert.ok(!this.content.document.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature"); + + Assert.ok(this.content.document.featurePolicy.allowsFeature("camera"), "Camera is allowed for self"); + Assert.ok(this.content.document.featurePolicy.allowsFeature("camera", "https://example.org"), "Camera is allowed for self"); + Assert.ok(!this.content.document.featurePolicy.allowsFeature("camera", "https://foo.bar"), "Camera is not allowed for a random URL"); + let allowed = this.content.document.featurePolicy.getAllowlistForFeature("camera"); + Assert.equal(allowed.length, 1, "Only 1 entry in allowlist for camera"); + Assert.equal(allowed[0], "https://example.org", "allowlist is 'self'"); + + Assert.ok(!this.content.document.featurePolicy.allowsFeature("geolocation"), "Geolocation is not allowed for self"); + Assert.ok(!this.content.document.featurePolicy.allowsFeature("geolocation", "https://example.org"), + "Geolocation is not allowed for self"); + Assert.ok(!this.content.document.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL"); + allowed = this.content.document.featurePolicy.getAllowlistForFeature("geolocation"); + Assert.equal(allowed.length, 0, "No allowlist for geolocation"); + + Assert.ok(this.content.document.featurePolicy.allowsFeature("microphone"), "Microphone is enabled for self"); + Assert.ok(this.content.document.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is enabled for self"); + Assert.ok(!this.content.document.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar"); + Assert.ok(!this.content.document.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com"); + allowed = this.content.document.featurePolicy.getAllowlistForFeature("microphone"); + Assert.equal(allowed.length, 1, "Only 1 entry in allowlist for microphone"); + Assert.equal(allowed[0], "https://example.org", "allowlist is self"); + + Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self"); + Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr", "https://example.org"), "Vibrate is disabled for self"); + Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar"); + allowed = this.content.document.featurePolicy.getAllowlistForFeature("vr"); + Assert.equal(allowed.length, 0, "No allowlist for vr"); + + Assert.ok(this.content.document.featurePolicy.allowedFeatures().includes("camera"), "Camera is allowed"); + Assert.ok(!this.content.document.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is not allowed"); + // microphone is enabled for this origin + Assert.ok(this.content.document.featurePolicy.allowedFeatures().includes("microphone"), "microphone is allowed"); + // vr is disabled everywhere. + Assert.ok(!this.content.document.featurePolicy.allowedFeatures().includes("vr"), "VR is not allowed"); + }); + + next(); + }; + document.body.appendChild(ifr); +} + +function test_cross_iframe_contentDocument_allow() { + info("Checking cross iframe document.featurePolicy with allow"); + + let ifr = document.createElement("iframe"); + ifr.setAttribute("src", "https://example.org/tests/dom/security/featurePolicy/test/mochitest/empty.html"); + ifr.setAttribute("allow", "geolocation; camera 'none'"); + ifr.onload = async function() { + await SpecialPowers.spawn(ifr, [], () => { + Assert.ok("featurePolicy" in this.content.document, "We have this.content.document.featurePolicy"); + + Assert.ok(!this.content.document.featurePolicy.allowsFeature("foobar"), "Random feature"); + Assert.ok(!this.content.document.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature"); + + Assert.ok(!this.content.document.featurePolicy.allowsFeature("camera"), "Camera is not allowed"); + let allowed = this.content.document.featurePolicy.getAllowlistForFeature("camera"); + Assert.equal(allowed.length, 0, "Camera has an empty allowlist"); + + Assert.ok(this.content.document.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self"); + Assert.ok(this.content.document.featurePolicy.allowsFeature("geolocation", "https://example.org"), "Geolocation is allowed for self"); + Assert.ok(!this.content.document.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL"); + allowed = this.content.document.featurePolicy.getAllowlistForFeature("geolocation"); + Assert.equal(allowed.length, 1, "Only 1 entry in allowlist for geolocation"); + Assert.equal(allowed[0], "https://example.org", "allowlist is '*'"); + + Assert.ok(this.content.document.featurePolicy.allowsFeature("microphone"), "Microphone is enabled for self"); + Assert.ok(this.content.document.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is enabled for self"); + Assert.ok(!this.content.document.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar"); + Assert.ok(!this.content.document.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com"); + allowed = this.content.document.featurePolicy.getAllowlistForFeature("microphone"); + Assert.equal(allowed.length, 1, "Only 1 entry in allowlist for microphone"); + Assert.equal(allowed[0], "https://example.org", "allowlist is self"); + + Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self"); + Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr", "https://example.org"), "Vibrate is disabled for self"); + Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar"); + allowed = this.content.document.featurePolicy.getAllowlistForFeature("vr"); + Assert.equal(allowed.length, 0, "No allowlist for vr"); + + Assert.ok(this.content.document.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is allowed only for self"); + // microphone is enabled for this origin + Assert.ok(this.content.document.featurePolicy.allowedFeatures().includes("microphone"), "microphone is allowed"); + }); + + next(); + }; + document.body.appendChild(ifr); +} + + +var tests = [ + test_document, + test_iframe_without_allow, + test_iframe_with_allow, + test_iframe_contentDocument, + test_cross_iframe_without_allow, + test_cross_iframe_with_allow, + test_cross_iframe_contentDocument_no_allow, + test_cross_iframe_contentDocument_allow +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +next(); + +</script> +</body> +</html> diff --git a/dom/security/featurepolicy/test/mochitest/test_parser.html^headers^ b/dom/security/featurepolicy/test/mochitest/test_parser.html^headers^ new file mode 100644 index 0000000000..949de013d3 --- /dev/null +++ b/dom/security/featurepolicy/test/mochitest/test_parser.html^headers^ @@ -0,0 +1 @@ +Feature-Policy: camera *; geolocation 'self'; microphone https://example.com https://example.org; vr 'none' diff --git a/dom/security/fuzztest/csp_fuzzer.cpp b/dom/security/fuzztest/csp_fuzzer.cpp new file mode 100644 index 0000000000..24f938cb1f --- /dev/null +++ b/dom/security/fuzztest/csp_fuzzer.cpp @@ -0,0 +1,41 @@ +/* -*- 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 https://mozilla.org/MPL/2.0/. */ + +#include "FuzzingInterface.h" +#include "mozilla/BasePrincipal.h" +#include "nsComponentManagerUtils.h" +#include "nsCSPContext.h" +#include "nsNetUtil.h" +#include "nsStringFwd.h" + +static int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + nsresult ret; + nsCOMPtr<nsIURI> selfURI; + ret = NS_NewURI(getter_AddRefs(selfURI), "http://selfuri.com"); + if (ret != NS_OK) return 0; + + mozilla::OriginAttributes attrs; + nsCOMPtr<nsIPrincipal> selfURIPrincipal = + mozilla::BasePrincipal::CreateContentPrincipal(selfURI, attrs); + if (!selfURIPrincipal) return 0; + + nsCOMPtr<nsIContentSecurityPolicy> csp = + do_CreateInstance(NS_CSPCONTEXT_CONTRACTID, &ret); + if (ret != NS_OK) return 0; + + ret = + csp->SetRequestContextWithPrincipal(selfURIPrincipal, selfURI, u""_ns, 0); + if (ret != NS_OK) return 0; + + NS_ConvertASCIItoUTF16 policy(reinterpret_cast<const char*>(data), size); + if (!policy.get()) return 0; + csp->AppendPolicy(policy, false, false); + + return 0; +} + +MOZ_FUZZING_INTERFACE_RAW(nullptr, LLVMFuzzerTestOneInput, + ContentSecurityPolicyParser); diff --git a/dom/security/fuzztest/csp_fuzzer.dict b/dom/security/fuzztest/csp_fuzzer.dict new file mode 100644 index 0000000000..480165d929 --- /dev/null +++ b/dom/security/fuzztest/csp_fuzzer.dict @@ -0,0 +1,95 @@ +### dom/security/nsCSPParser.cpp +# tokens +":" +";" +"/" +"+" +"-" +"." +"_" +"~" +"*" +"'" +"#" +"?" +"%" +"!" +"$" +"&" +"(" +")" +"=" +"@" + +### https://www.w3.org/TR/{CSP,CSP2,CSP3}/ +# directive names +"default-src" +"script-src" +"object-src" +"style-src" +"img-src" +"media-src" +"frame-src" +"font-src" +"connect-src" +"report-uri" +"frame-ancestors" +"reflected-xss" +"base-uri" +"form-action" +"manifest-src" +"upgrade-insecure-requests" +"child-src" +"block-all-mixed-content" +"sandbox" +"worker-src" +"plugin-types" +"disown-opener" +"report-to" + +# directive values +"'self'" +"'unsafe-inline'" +"'unsafe-eval'" +"'none'" +"'strict-dynamic'" +"'unsafe-hashed-attributes'" +"'nonce-AA=='" +"'sha256-fw=='" +"'sha384-/w=='" +"'sha512-//8='" + +# subresources +"a" +"audio" +"embed" +"iframe" +"img" +"link" +"object" +"script" +"source" +"style" +"track" +"video" + +# sandboxing flags +"allow-forms" +"allow-pointer-lock" +"allow-popups" +"allow-same-origin" +"allow-scripts" +"allow-top-navigation" +"allow-top-navigation-by-user-activation" + +# URI components +"https:" +"ws:" +"blob:" +"data:" +"filesystem:" +"javascript:" +"http://" +"selfuri.com" +"127.0.0.1" +"::1" diff --git a/dom/security/fuzztest/moz.build b/dom/security/fuzztest/moz.build new file mode 100644 index 0000000000..3a1f3f4396 --- /dev/null +++ b/dom/security/fuzztest/moz.build @@ -0,0 +1,18 @@ +# -*- 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/. + +Library("FuzzingDOMSecurity") + +LOCAL_INCLUDES += [ + "/dom/security", + "/netwerk/base", +] + +include("/tools/fuzzing/libfuzzer-config.mozbuild") + +SOURCES += ["csp_fuzzer.cpp"] + +FINAL_LIBRARY = "xul-gtest" diff --git a/dom/security/moz.build b/dom/security/moz.build new file mode 100644 index 0000000000..0f2aed6a1f --- /dev/null +++ b/dom/security/moz.build @@ -0,0 +1,82 @@ +# -*- 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/. + +with Files("*"): + BUG_COMPONENT = ("Core", "DOM: Security") + +TEST_DIRS += ["test"] + +DIRS += ["featurepolicy", "sanitizer"] + +EXPORTS.mozilla.dom += [ + "CSPEvalChecker.h", + "DOMSecurityMonitor.h", + "FramingChecker.h", + "nsContentSecurityManager.h", + "nsContentSecurityUtils.h", + "nsCSPContext.h", + "nsCSPService.h", + "nsCSPUtils.h", + "nsHTTPSOnlyStreamListener.h", + "nsHTTPSOnlyUtils.h", + "nsMixedContentBlocker.h", + "PolicyTokenizer.h", + "ReferrerInfo.h", + "SecFetch.h", + "SRICheck.h", + "SRILogHelper.h", + "SRIMetadata.h", +] + +EXPORTS += [ + "nsContentSecurityManager.h", + "nsContentSecurityUtils.h", + "nsMixedContentBlocker.h", + "ReferrerInfo.h", +] + +UNIFIED_SOURCES += [ + "CSPEvalChecker.cpp", + "DOMSecurityMonitor.cpp", + "FramingChecker.cpp", + "nsContentSecurityManager.cpp", + "nsContentSecurityUtils.cpp", + "nsCSPContext.cpp", + "nsCSPParser.cpp", + "nsCSPService.cpp", + "nsCSPUtils.cpp", + "nsHTTPSOnlyStreamListener.cpp", + "nsHTTPSOnlyUtils.cpp", + "nsMixedContentBlocker.cpp", + "PolicyTokenizer.cpp", + "ReferrerInfo.cpp", + "SecFetch.cpp", + "SRICheck.cpp", + "SRIMetadata.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/caps", + "/docshell/base", # for nsDocShell.h + "/netwerk/base", + "/netwerk/protocol/data", # for nsDataHandler.h + "/netwerk/protocol/http", # for HttpBaseChannel.h +] + +include("/tools/fuzzing/libfuzzer-config.mozbuild") + +if CONFIG["FUZZING_INTERFACES"]: + TEST_DIRS += ["fuzztest"] + + +XPIDL_SOURCES += [ + "nsIHttpsOnlyModePermission.idl", +] + +XPIDL_MODULE = "dom_security" diff --git a/dom/security/nsCSPContext.cpp b/dom/security/nsCSPContext.cpp new file mode 100644 index 0000000000..be29a78146 --- /dev/null +++ b/dom/security/nsCSPContext.cpp @@ -0,0 +1,1999 @@ +/* -*- 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 <string> +#include <unordered_set> + +#include "nsCOMPtr.h" +#include "nsContentPolicyUtils.h" +#include "nsContentUtils.h" +#include "nsCSPContext.h" +#include "nsCSPParser.h" +#include "nsCSPService.h" +#include "nsGlobalWindowOuter.h" +#include "nsError.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIClassInfoImpl.h" +#include "mozilla/dom/Document.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 "nsISupportsPrimitives.h" +#include "nsIUploadChannel.h" +#include "nsIURIMutator.h" +#include "nsIScriptError.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/Preferences.h" +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/dom/CSPReportBinding.h" +#include "mozilla/dom/CSPDictionariesBinding.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "nsINetworkInterceptController.h" +#include "nsSandboxFlags.h" +#include "nsIScriptElement.h" +#include "nsIEventTarget.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/Element.h" +#include "nsXULAppAPI.h" +#include "nsJSUtils.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::ipc; + +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 LogModule* GetCspOriginLogLog() { + static LazyLogModule gCspOriginPRLog("CSPOrigin"); + return gCspOriginPRLog; +} + +#define CSPORIGINLOG(args) \ + MOZ_LOG(GetCspOriginLogLog(), mozilla::LogLevel::Debug, args) +#define CSPORIGINLOGENABLED() \ + MOZ_LOG_TEST(GetCspOriginLogLog(), mozilla::LogLevel::Debug) + +#ifdef DEBUG +/** + * This function is only used for verification purposes within + * GatherSecurityPolicyViolationEventData. + */ +static bool ValidateDirectiveName(const nsAString& aDirective) { + static const auto directives = []() { + std::unordered_set<std::string> directives; + constexpr size_t dirLen = + sizeof(CSPStrDirectives) / sizeof(CSPStrDirectives[0]); + for (size_t i = 0; i < dirLen; ++i) { + directives.insert(CSPStrDirectives[i]); + } + return directives; + }(); + + nsAutoString directive(aDirective); + auto itr = directives.find(NS_ConvertUTF16toUTF8(directive).get()); + return itr != directives.end(); +} +#endif // DEBUG + +static void BlockedContentSourceToString( + nsCSPContext::BlockedContentSource aSource, nsACString& aString) { + switch (aSource) { + case nsCSPContext::BlockedContentSource::eUnknown: + aString.Truncate(); + break; + + case nsCSPContext::BlockedContentSource::eInline: + aString.AssignLiteral("inline"); + break; + + case nsCSPContext::BlockedContentSource::eEval: + aString.AssignLiteral("eval"); + break; + + case nsCSPContext::BlockedContentSource::eSelf: + aString.AssignLiteral("self"); + break; + + case nsCSPContext::BlockedContentSource::eWasmEval: + aString.AssignLiteral("wasm-eval"); + break; + } +} + +/* ===== nsIContentSecurityPolicy impl ====== */ + +NS_IMETHODIMP +nsCSPContext::ShouldLoad(nsContentPolicyType aContentType, + nsICSPEventListener* aCSPEventListener, + nsIURI* aContentLocation, + nsIURI* aOriginalURIIfRedirect, + bool aSendViolationReports, const nsAString& aNonce, + bool aParserCreated, int16_t* outDecision) { + if (CSPCONTEXTLOGENABLED()) { + CSPCONTEXTLOG(("nsCSPContext::ShouldLoad, aContentLocation: %s", + aContentLocation->GetSpecOrDefault().get())); + CSPCONTEXTLOG((">>>> aContentType: %d", aContentType)); + } + + // 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 allowlisted scheme (about: chrome:, etc). + // * CSP is enabled + // * Content Type is not allowlisted (CSP Reports, TYPE_DOCUMENT, etc). + // * Fast Path for Apps + + // 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; + } + + bool permitted = permitsInternal( + dir, + nullptr, // aTriggeringElement + aCSPEventListener, aContentLocation, aOriginalURIIfRedirect, aNonce, + false, // allow fallback to default-src + aSendViolationReports, + true, // send blocked URI in violation reports + aParserCreated); + + *outDecision = + permitted ? nsIContentPolicy::ACCEPT : nsIContentPolicy::REJECT_SERVER; + + if (CSPCONTEXTLOGENABLED()) { + CSPCONTEXTLOG( + ("nsCSPContext::ShouldLoad, decision: %s, " + "aContentLocation: %s", + *outDecision > 0 ? "load" : "deny", + aContentLocation->GetSpecOrDefault().get())); + } + return NS_OK; +} + +bool nsCSPContext::permitsInternal( + CSPDirective aDir, Element* aTriggeringElement, + nsICSPEventListener* aCSPEventListener, nsIURI* aContentLocation, + nsIURI* aOriginalURIIfRedirect, const nsAString& aNonce, bool aSpecific, + bool aSendViolationReports, bool aSendContentLocationInViolationReports, + bool aParserCreated) { + EnsureIPCPoliciesRead(); + bool permits = true; + + nsAutoString violatedDirective; + for (uint32_t p = 0; p < mPolicies.Length(); p++) { + if (!mPolicies[p]->permits(aDir, aContentLocation, aNonce, + !!aOriginalURIIfRedirect, 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; + } + + // In CSP 3.0 the effective directive doesn't become the actually used + // directive in case of a fallback from e.g. script-src-elem to + // script-src or default-src. + nsAutoString effectiveDirective; + effectiveDirective.AssignASCII(CSP_CSPDirectiveToString(aDir)); + + // Callers should set |aSendViolationReports| to false 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 (aSendViolationReports) { + uint32_t lineNumber = 0; + uint32_t columnNumber = 0; + nsAutoString spec; + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (cx) { + nsJSUtils::GetCallingLocation(cx, spec, &lineNumber, &columnNumber); + // If GetCallingLocation fails linenumber & columnNumber are set to 0 + // anyway so we can skip checking if that is the case. + } + AsyncReportViolation( + aTriggeringElement, aCSPEventListener, + (aSendContentLocationInViolationReports ? aContentLocation + : nullptr), + BlockedContentSource::eUnknown, /* a BlockedContentSource */ + aOriginalURIIfRedirect, /* in case of redirect originalURI is not + null */ + violatedDirective, effectiveDirective, p, /* policy index */ + u""_ns, /* no observer subject */ + spec, /* source file */ + false, // aReportSample (no sample) + u""_ns, /* no script sample */ + lineNumber, /* line number */ + columnNumber); /* column 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), + mSkipAllowInlineStyleCheck(false), + 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]; + } +} + +/* static */ +bool nsCSPContext::Equals(nsIContentSecurityPolicy* aCSP, + nsIContentSecurityPolicy* aOtherCSP) { + if (aCSP == aOtherCSP) { + // fast path for pointer equality + return true; + } + + uint32_t policyCount = 0; + if (aCSP) { + aCSP->GetPolicyCount(&policyCount); + } + + uint32_t otherPolicyCount = 0; + if (aOtherCSP) { + aOtherCSP->GetPolicyCount(&otherPolicyCount); + } + + if (policyCount != otherPolicyCount) { + return false; + } + + nsAutoString policyStr, otherPolicyStr; + for (uint32_t i = 0; i < policyCount; ++i) { + aCSP->GetPolicyString(i, policyStr); + aOtherCSP->GetPolicyString(i, otherPolicyStr); + if (!policyStr.Equals(otherPolicyStr)) { + return false; + } + } + + return true; +} + +nsresult nsCSPContext::InitFromOther(nsCSPContext* aOtherContext) { + NS_ENSURE_ARG(aOtherContext); + + nsresult rv = NS_OK; + nsCOMPtr<Document> doc = do_QueryReferent(aOtherContext->mLoadingContext); + if (doc) { + rv = SetRequestContextWithDocument(doc); + } else { + rv = SetRequestContextWithPrincipal( + aOtherContext->mLoadingPrincipal, aOtherContext->mSelfURI, + aOtherContext->mReferrer, aOtherContext->mInnerWindowID); + } + NS_ENSURE_SUCCESS(rv, rv); + + mSkipAllowInlineStyleCheck = aOtherContext->mSkipAllowInlineStyleCheck; + + // This policy was already parsed somewhere else, don't emit parsing errors. + mSuppressParserLogMessages = true; + for (auto policy : aOtherContext->mPolicies) { + nsAutoString policyStr; + policy->toString(policyStr); + AppendPolicy(policyStr, policy->getReportOnlyFlag(), + policy->getDeliveredViaMetaTagFlag()); + } + + mSuppressParserLogMessages = aOtherContext->mSuppressParserLogMessages; + + mIPCPolicies = aOtherContext->mIPCPolicies.Clone(); + return NS_OK; +} + +void nsCSPContext::EnsureIPCPoliciesRead() { + // Most likely the parser errors already happened before serializing + // the policy for IPC. + bool previous = mSuppressParserLogMessages; + mSuppressParserLogMessages = true; + + if (mIPCPolicies.Length() > 0) { + nsresult rv; + for (auto& policy : mIPCPolicies) { + rv = AppendPolicy(policy.policy(), policy.reportOnlyFlag(), + policy.deliveredViaMetaTagFlag()); + Unused << NS_WARN_IF(NS_FAILED(rv)); + } + mIPCPolicies.Clear(); + } + + mSuppressParserLogMessages = previous; +} + +NS_IMETHODIMP +nsCSPContext::GetPolicyString(uint32_t aIndex, nsAString& outStr) { + outStr.Truncate(); + EnsureIPCPoliciesRead(); + if (aIndex >= mPolicies.Length()) { + return NS_ERROR_ILLEGAL_VALUE; + } + mPolicies[aIndex]->toString(outStr); + return NS_OK; +} + +const nsCSPPolicy* nsCSPContext::GetPolicy(uint32_t aIndex) { + EnsureIPCPoliciesRead(); + if (aIndex >= mPolicies.Length()) { + return nullptr; + } + return mPolicies[aIndex]; +} + +NS_IMETHODIMP +nsCSPContext::GetPolicyCount(uint32_t* outPolicyCount) { + EnsureIPCPoliciesRead(); + *outPolicyCount = mPolicies.Length(); + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::GetUpgradeInsecureRequests(bool* outUpgradeRequest) { + EnsureIPCPoliciesRead(); + *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) { + EnsureIPCPoliciesRead(); + *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::GetEnforcesFrameAncestors(bool* outEnforcesFrameAncestors) { + EnsureIPCPoliciesRead(); + *outEnforcesFrameAncestors = false; + for (uint32_t i = 0; i < mPolicies.Length(); i++) { + if (!mPolicies[i]->getReportOnlyFlag() && + mPolicies[i]->hasDirective( + nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE)) { + *outEnforcesFrameAncestors = true; + return NS_OK; + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::AppendPolicy(const nsAString& aPolicyString, bool aReportOnly, + bool aDeliveredViaMetaTag) { + CSPCONTEXTLOG(("nsCSPContext::AppendPolicy: %s", + NS_ConvertUTF16toUTF8(aPolicyString).get())); + + // Use mSelfURI from setRequestContextWith{Document,Principal} (bug 991474) + MOZ_ASSERT( + mLoadingPrincipal, + "did you forget to call setRequestContextWith{Document,Principal}?"); + MOZ_ASSERT( + mSelfURI, + "did you forget to call setRequestContextWith{Document,Principal}?"); + NS_ENSURE_TRUE(mLoadingPrincipal, NS_ERROR_UNEXPECTED); + NS_ENSURE_TRUE(mSelfURI, NS_ERROR_UNEXPECTED); + + if (CSPORIGINLOGENABLED()) { + nsAutoCString selfURISpec; + mSelfURI->GetSpec(selfURISpec); + CSPORIGINLOG(("CSP - AppendPolicy")); + CSPORIGINLOG((" * selfURI: %s", selfURISpec.get())); + CSPORIGINLOG((" * reportOnly: %s", aReportOnly ? "yes" : "no")); + CSPORIGINLOG( + (" * deliveredViaMetaTag: %s", aDeliveredViaMetaTag ? "yes" : "no")); + CSPORIGINLOG( + (" * policy: %s\n", NS_ConvertUTF16toUTF8(aPolicyString).get())); + } + + nsCSPPolicy* policy = nsCSPParser::parseContentSecurityPolicy( + aPolicyString, mSelfURI, aReportOnly, this, aDeliveredViaMetaTag, + mSuppressParserLogMessages); + if (policy) { + if (policy->hasDirective( + nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE)) { + nsAutoCString selfURIspec, referrer; + if (mSelfURI) { + mSelfURI->GetAsciiSpec(selfURIspec); + } + CopyUTF16toUTF8(mReferrer, referrer); + CSPCONTEXTLOG( + ("nsCSPContext::AppendPolicy added UPGRADE_IF_INSECURE_DIRECTIVE " + "self-uri=%s referrer=%s", + selfURIspec.get(), referrer.get())); + } + + mPolicies.AppendElement(policy); + + // set the flag on the document for CSP telemetry + nsCOMPtr<Document> doc = do_QueryReferent(mLoadingContext); + if (doc) { + doc->SetHasCSP(true); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::GetAllowsEval(bool* outShouldReportViolation, + bool* outAllowsEval) { + EnsureIPCPoliciesRead(); + *outShouldReportViolation = false; + *outAllowsEval = true; + + for (uint32_t i = 0; i < mPolicies.Length(); i++) { + if (!mPolicies[i]->allows(SCRIPT_SRC_DIRECTIVE, CSP_UNSAFE_EVAL, u""_ns, + 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; +} + +NS_IMETHODIMP +nsCSPContext::GetAllowsWasmEval(bool* outShouldReportViolation, + bool* outAllowsWasmEval) { + EnsureIPCPoliciesRead(); + *outShouldReportViolation = false; + *outAllowsWasmEval = true; + + if (!StaticPrefs::security_csp_wasm_unsafe_eval_enabled()) { + // Allow and don't report when wasm-unsafe-eval isn't supported. + return NS_OK; + } + + for (uint32_t i = 0; i < mPolicies.Length(); i++) { + // Either 'unsafe-eval' or 'wasm-unsafe-eval' can allow this + if (!mPolicies[i]->allows(SCRIPT_SRC_DIRECTIVE, CSP_WASM_UNSAFE_EVAL, + u""_ns, false) && + !mPolicies[i]->allows(SCRIPT_SRC_DIRECTIVE, CSP_UNSAFE_EVAL, u""_ns, + 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()) { + *outAllowsWasmEval = false; + } + } + } + + return NS_OK; +} + +// Helper function to report inline violations +void nsCSPContext::reportInlineViolation( + CSPDirective aDirective, Element* aTriggeringElement, + nsICSPEventListener* aCSPEventListener, const nsAString& aNonce, + bool aReportSample, const nsAString& aSample, + const nsAString& aViolatedDirective, const nsAString& aEffectiveDirective, + uint32_t aViolatedPolicyIndex, // TODO, use report only flag for that + uint32_t aLineNumber, uint32_t aColumnNumber) { + 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 = (aDirective == SCRIPT_SRC_ELEM_DIRECTIVE || + aDirective == SCRIPT_SRC_ATTR_DIRECTIVE) + ? NS_LITERAL_STRING_FROM_CSTRING( + SCRIPT_NONCE_VIOLATION_OBSERVER_TOPIC) + : NS_LITERAL_STRING_FROM_CSTRING( + STYLE_NONCE_VIOLATION_OBSERVER_TOPIC); + } else { + observerSubject = (aDirective == SCRIPT_SRC_ELEM_DIRECTIVE || + aDirective == SCRIPT_SRC_ATTR_DIRECTIVE) + ? NS_LITERAL_STRING_FROM_CSTRING( + SCRIPT_HASH_VIOLATION_OBSERVER_TOPIC) + : NS_LITERAL_STRING_FROM_CSTRING( + STYLE_HASH_VIOLATION_OBSERVER_TOPIC); + } + + nsAutoString sourceFile; + uint32_t lineNumber; + uint32_t columnNumber; + + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (!cx || !nsJSUtils::GetCallingLocation(cx, sourceFile, &lineNumber, + &columnNumber)) { + // use selfURI as the sourceFile + if (mSelfURI) { + nsAutoCString cSourceFile; + mSelfURI->GetSpec(cSourceFile); + sourceFile.Assign(NS_ConvertUTF8toUTF16(cSourceFile)); + } + lineNumber = aLineNumber; + columnNumber = aColumnNumber; + } + + AsyncReportViolation(aTriggeringElement, aCSPEventListener, + nullptr, // aBlockedURI + BlockedContentSource::eInline, // aBlockedSource + mSelfURI, // aOriginalURI + aViolatedDirective, // aViolatedDirective + aEffectiveDirective, // aEffectiveDirective + aViolatedPolicyIndex, // aViolatedPolicyIndex + observerSubject, // aObserverSubject + sourceFile, // aSourceFile + aReportSample, // aReportSample + aSample, // aScriptSample + lineNumber, // aLineNum + columnNumber); // aColumnNum +} + +NS_IMETHODIMP +nsCSPContext::GetAllowsInline(CSPDirective aDirective, bool aHasUnsafeHash, + const nsAString& aNonce, bool aParserCreated, + Element* aTriggeringElement, + nsICSPEventListener* aCSPEventListener, + const nsAString& aContentOfPseudoScript, + uint32_t aLineNumber, uint32_t aColumnNumber, + bool* outAllowsInline) { + *outAllowsInline = true; + + if (aDirective != SCRIPT_SRC_ELEM_DIRECTIVE && + aDirective != SCRIPT_SRC_ATTR_DIRECTIVE && + aDirective != STYLE_SRC_ELEM_DIRECTIVE && + aDirective != STYLE_SRC_ATTR_DIRECTIVE) { + MOZ_ASSERT(false, + "can only allow inline for (script/style)-src-(attr/elem)"); + return NS_OK; + } + + EnsureIPCPoliciesRead(); + nsAutoString content(u""_ns); + + // 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(aDirective, CSP_UNSAFE_INLINE, u""_ns, + aParserCreated) || + mPolicies[i]->allows(aDirective, CSP_NONCE, aNonce, aParserCreated); + + // If the inlined script or style is allowed by either unsafe-inline or the + // nonce, go ahead and shortcut this loop so we can avoid allocating + // unecessary strings + if (allowed) { + continue; + } + + // Check the content length to ensure the content is not allocated more than + // once. Even though we are in a for loop, it is probable that there is only + // one policy, so this check may be unnecessary. + if (content.IsEmpty() && aTriggeringElement) { + nsCOMPtr<nsIScriptElement> element = + do_QueryInterface(aTriggeringElement); + if (element) { + element->GetScriptText(content); + } + } + if (content.IsEmpty()) { + content = aContentOfPseudoScript; + } + + // Per https://w3c.github.io/webappsec-csp/#match-element-to-source-list + // Step 5. If type is "script" or "style", or unsafe-hashes flag is true: + // + // aHasUnsafeHash is true for event handlers (type "script attribute"), + // style= attributes (type "style attribute") and the javascript: protocol. + if (!aHasUnsafeHash || mPolicies[i]->allows(aDirective, CSP_UNSAFE_HASHES, + u""_ns, aParserCreated)) { + allowed = + mPolicies[i]->allows(aDirective, CSP_HASH, content, 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; + bool reportSample = false; + mPolicies[i]->getDirectiveStringAndReportSampleForContentType( + aDirective, violatedDirective, &reportSample); + + // In CSP 3.0 the effective directive doesn't become the actually used + // directive in case of a fallback from e.g. script-src-elem to + // script-src or default-src. + nsAutoString effectiveDirective; + effectiveDirective.AssignASCII(CSP_CSPDirectiveToString(aDirective)); + + reportInlineViolation(aDirective, aTriggeringElement, aCSPEventListener, + aNonce, reportSample, content, violatedDirective, + effectiveDirective, i, aLineNumber, aColumnNumber); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::GetAllowsNavigateTo(nsIURI* aURI, bool aIsFormSubmission, + bool aWasRedirected, bool aEnforceAllowlist, + bool* outAllowsNavigateTo) { + /* + * The matrix below shows the different values of (aWasRedirect, + * aEnforceAllowlist) for the three different checks we do. + * + * Navigation | Start Loading | Initiate Redirect | Document + * | (nsDocShell) | (nsCSPService) | + * ----------------------------------------------------------------- + * A -> B (false,false) - (false,true) + * A -> ... -> B (false,false) (true,false) (true,true) + */ + *outAllowsNavigateTo = false; + + EnsureIPCPoliciesRead(); + // The 'form-action' directive overrules 'navigate-to' for form submissions. + // So in case this is a form submission and the directive 'form-action' is + // present then there is nothing for us to do here, see: 6.3.3.1.2 + // https://www.w3.org/TR/CSP3/#navigate-to-pre-navigate + if (aIsFormSubmission) { + for (unsigned long i = 0; i < mPolicies.Length(); i++) { + if (mPolicies[i]->hasDirective( + nsIContentSecurityPolicy::FORM_ACTION_DIRECTIVE)) { + *outAllowsNavigateTo = true; + return NS_OK; + } + } + } + + bool atLeastOneBlock = false; + for (unsigned long i = 0; i < mPolicies.Length(); i++) { + if (!mPolicies[i]->allowsNavigateTo(aURI, aWasRedirected, + aEnforceAllowlist)) { + if (!mPolicies[i]->getReportOnlyFlag()) { + atLeastOneBlock = true; + } + + // If the load encountered a server side redirect, the spec suggests to + // remove the path component from the URI, see: + // https://www.w3.org/TR/CSP3/#source-list-paths-and-redirects + nsCOMPtr<nsIURI> blockedURIForReporting = aURI; + if (aWasRedirected) { + nsAutoCString prePathStr; + nsCOMPtr<nsIURI> prePathURI; + nsresult rv = aURI->GetPrePath(prePathStr); + NS_ENSURE_SUCCESS(rv, rv); + rv = NS_NewURI(getter_AddRefs(blockedURIForReporting), prePathStr); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Lines numbers and source file for the violation report + uint32_t lineNumber = 0; + uint32_t columnNumber = 0; + nsAutoCString spec; + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (cx) { + nsJSUtils::GetCallingLocation(cx, spec, &lineNumber, &columnNumber); + // If GetCallingLocation fails linenumber & columnNumber are set to 0 + // anyway so we can skip checking if that is the case. + } + + // Report the violation + nsresult rv = AsyncReportViolation( + nullptr, // aTriggeringElement + nullptr, // aCSPEventListener + blockedURIForReporting, // aBlockedURI + nsCSPContext::BlockedContentSource::eSelf, // aBlockedSource + nullptr, // aOriginalURI + u"navigate-to"_ns, // aViolatedDirective + u"navigate-to"_ns, // aEffectiveDirective + i, // aViolatedPolicyIndex + u""_ns, // aObserverSubject + NS_ConvertUTF8toUTF16(spec), // aSourceFile + false, // aReportSample + u""_ns, // aScriptSample + lineNumber, // aLineNum + columnNumber); // aColumnNum + NS_ENSURE_SUCCESS(rv, rv); + } + } + + *outAllowsNavigateTo = !atLeastOneBlock; + return NS_OK; +} + +/** + * 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 aColumnNum + * source column 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, Element* aTriggeringElement, + nsICSPEventListener* aCSPEventListener, const nsAString& aSourceFile, + const nsAString& aScriptSample, int32_t aLineNum, int32_t aColumnNum, + const nsAString& aNonce, const nsAString& aContent) { + EnsureIPCPoliciesRead(); + + BlockedContentSource blockedContentSource; + enum CSPKeyword keyword; + nsAutoString observerSubject; + if (aViolationType == nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL) { + blockedContentSource = BlockedContentSource::eEval; + keyword = CSP_UNSAFE_EVAL; + observerSubject.AssignLiteral(EVAL_VIOLATION_OBSERVER_TOPIC); + } else { + NS_ASSERTION( + aViolationType == nsIContentSecurityPolicy::VIOLATION_TYPE_WASM_EVAL, + "unexpected aViolationType"); + blockedContentSource = BlockedContentSource::eWasmEval; + keyword = CSP_WASM_UNSAFE_EVAL; + observerSubject.AssignLiteral(WASM_EVAL_VIOLATION_OBSERVER_TOPIC); + } + + for (uint32_t p = 0; p < mPolicies.Length(); p++) { + NS_ASSERTION(mPolicies[p], "null pointer in nsTArray<nsCSPPolicy>"); + + if (mPolicies[p]->allows(SCRIPT_SRC_DIRECTIVE, keyword, u""_ns, false)) { + continue; + } + + nsAutoString violatedDirective; + bool reportSample = false; + mPolicies[p]->getDirectiveStringAndReportSampleForContentType( + SCRIPT_SRC_DIRECTIVE, violatedDirective, &reportSample); + + AsyncReportViolation(aTriggeringElement, aCSPEventListener, nullptr, + blockedContentSource, nullptr, violatedDirective, + u"script-src"_ns /* aEffectiveDirective */, p, + observerSubject, aSourceFile, reportSample, + aScriptSample, aLineNum, aColumnNum); + } + return NS_OK; +} + +#undef CASE_CHECK_AND_REPORT + +NS_IMETHODIMP +nsCSPContext::SetRequestContextWithDocument(Document* aDocument) { + MOZ_ASSERT(aDocument, "Can't set context without doc"); + NS_ENSURE_ARG(aDocument); + + mLoadingContext = do_GetWeakReference(aDocument); + mSelfURI = aDocument->GetDocumentURI(); + mLoadingPrincipal = aDocument->NodePrincipal(); + aDocument->GetReferrer(mReferrer); + mInnerWindowID = aDocument->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 = aDocument->GetDocumentLoadGroup(); + // set the flag on the document for CSP telemetry + mEventTarget = aDocument->EventTargetFor(TaskCategory::Other); + + MOZ_ASSERT(mLoadingPrincipal, "need a valid requestPrincipal"); + MOZ_ASSERT(mSelfURI, "need mSelfURI to translate 'self' into actual URI"); + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::SetRequestContextWithPrincipal(nsIPrincipal* aRequestPrincipal, + nsIURI* aSelfURI, + const nsAString& aReferrer, + uint64_t aInnerWindowId) { + NS_ENSURE_ARG(aRequestPrincipal); + + mLoadingPrincipal = aRequestPrincipal; + mSelfURI = aSelfURI; + mReferrer = aReferrer; + mInnerWindowID = aInnerWindowId; + // if no document is available, then it also does not make sense to queue + // console messages sending messages to the browser console instead of the web + // console in that case. + mQueueUpMessages = false; + mCallingChannelLoadGroup = nullptr; + mEventTarget = nullptr; + + MOZ_ASSERT(mLoadingPrincipal, "need a valid requestPrincipal"); + MOZ_ASSERT(mSelfURI, "need mSelfURI to translate 'self' into actual URI"); + return NS_OK; +} + +nsIPrincipal* nsCSPContext::GetRequestPrincipal() { return mLoadingPrincipal; } + +nsIURI* nsCSPContext::GetSelfURI() { return mSelfURI; } + +NS_IMETHODIMP +nsCSPContext::GetReferrer(nsAString& outReferrer) { + outReferrer.Truncate(); + outReferrer.Append(mReferrer); + return NS_OK; +} + +uint64_t nsCSPContext::GetInnerWindowID() { return mInnerWindowID; } + +bool nsCSPContext::GetSkipAllowInlineStyleCheck() { + return mSkipAllowInlineStyleCheck; +} + +void nsCSPContext::SetSkipAllowInlineStyleCheck( + bool aSkipAllowInlineStyleCheck) { + mSkipAllowInlineStyleCheck = aSkipAllowInlineStyleCheck; +} + +NS_IMETHODIMP +nsCSPContext::EnsureEventTarget(nsIEventTarget* aEventTarget) { + NS_ENSURE_ARG(aEventTarget); + // Don't bother if we did have a valid event target (if the csp object is + // tied to a document in SetRequestContextWithDocument) + if (mEventTarget) { + return NS_OK; + } + + mEventTarget = aEventTarget; + return NS_OK; +} + +struct ConsoleMsgQueueElem { + nsString mMsg; + nsString mSourceName; + nsString mSourceLine; + uint32_t mLineNumber; + uint32_t mColumnNumber; + uint32_t mSeverityFlag; + nsCString mCategory; +}; + +void nsCSPContext::flushConsoleMessages() { + bool privateWindow = false; + + // should flush messages even if doc is not available + nsCOMPtr<Document> doc = do_QueryReferent(mLoadingContext); + if (doc) { + mInnerWindowID = doc->InnerWindowID(); + privateWindow = + !!doc->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId; + } + + 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, + elem.mCategory, mInnerWindowID, privateWindow); + } + mConsoleMsgQueue.Clear(); +} + +void nsCSPContext::logToConsole(const char* aName, + const nsTArray<nsString>& aParams, + const nsAString& aSourceName, + const nsAString& aSourceLine, + uint32_t aLineNumber, uint32_t aColumnNumber, + uint32_t aSeverityFlag) { + // we are passing aName as the category so we can link to the + // appropriate MDN docs depending on the specific error. + nsDependentCString category(aName); + + // Fallback + nsAutoString sourceName(aSourceName); + if (sourceName.IsEmpty() && mSelfURI) { + nsAutoCString spec; + mSelfURI->GetSpec(spec); + CopyUTF8toUTF16(spec, sourceName); + } + + // let's check if we have to queue up console messages + if (mQueueUpMessages) { + nsAutoString msg; + CSP_GetLocalizedStr(aName, aParams, msg); + ConsoleMsgQueueElem& elem = *mConsoleMsgQueue.AppendElement(); + elem.mMsg = msg; + elem.mSourceName = PromiseFlatString(sourceName); + elem.mSourceLine = PromiseFlatString(aSourceLine); + elem.mLineNumber = aLineNumber; + elem.mColumnNumber = aColumnNumber; + elem.mSeverityFlag = aSeverityFlag; + elem.mCategory = category; + return; + } + + bool privateWindow = false; + nsCOMPtr<Document> doc = do_QueryReferent(mLoadingContext); + if (doc) { + privateWindow = + !!doc->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId; + } + + CSP_LogLocalizedStr(aName, aParams, sourceName, aSourceLine, aLineNumber, + aColumnNumber, aSeverityFlag, category, mInnerWindowID, + privateWindow); +} + +/** + * Strip URI for reporting according to: + * https://w3c.github.io/webappsec-csp/#security-violation-reports + * + * @param aSelfURI + * The URI of the CSP policy. Used for cross-origin checks. + * @param aURI + * The URI of the blocked resource. In case of a redirect, this it the + * initial URI the request started out with, not the redirected URI. + * @param aEffectiveDirective + * The effective directive that triggered this report + * @return The ASCII serialization of the uri to be reported ignoring + * the ref part of the URI. + */ +void StripURIForReporting(nsIURI* aSelfURI, nsIURI* aURI, + const nsAString& aEffectiveDirective, + nsACString& outStrippedURI) { + // If the origin of aURI 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 isHttpFtpOrWs = + (aURI->SchemeIs("http") || aURI->SchemeIs("https") || + aURI->SchemeIs("ftp") || aURI->SchemeIs("ws") || aURI->SchemeIs("wss")); + + if (!isHttpFtpOrWs) { + // 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; + } + + // For cross-origin URIs in frame-src also strip the path. + // This prevents detailed tracking of pages loaded into an iframe + // by the embedding page using a report-only policy. + if (aEffectiveDirective.EqualsLiteral("frame-src") || + aEffectiveDirective.EqualsLiteral("object-src")) { + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + if (NS_FAILED(ssm->CheckSameOriginURI(aSelfURI, aURI, false, false))) { + aURI->GetPrePath(outStrippedURI); + return; + } + } + + // Return aURI, with any fragment component removed. + aURI->GetSpecIgnoringRef(outStrippedURI); +} + +nsresult nsCSPContext::GatherSecurityPolicyViolationEventData( + nsIURI* aBlockedURI, const nsACString& aBlockedString, nsIURI* aOriginalURI, + const nsAString& aEffectiveDirective, uint32_t aViolatedPolicyIndex, + const nsAString& aSourceFile, const nsAString& aScriptSample, + uint32_t aLineNum, uint32_t aColumnNum, + mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit) { + EnsureIPCPoliciesRead(); + NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1); + + MOZ_ASSERT(ValidateDirectiveName(aEffectiveDirective), + "Invalid directive name"); + + nsresult rv; + + // document-uri + nsAutoCString reportDocumentURI; + StripURIForReporting(mSelfURI, mSelfURI, aEffectiveDirective, + reportDocumentURI); + CopyUTF8toUTF16(reportDocumentURI, aViolationEventInit.mDocumentURI); + + // referrer + aViolationEventInit.mReferrer = mReferrer; + + // blocked-uri + if (aBlockedURI) { + nsAutoCString reportBlockedURI; + StripURIForReporting(mSelfURI, aOriginalURI ? aOriginalURI : aBlockedURI, + aEffectiveDirective, reportBlockedURI); + CopyUTF8toUTF16(reportBlockedURI, aViolationEventInit.mBlockedURI); + } else { + CopyUTF8toUTF16(aBlockedString, aViolationEventInit.mBlockedURI); + } + + // effective-directive + // The name of the policy directive that was violated. + aViolationEventInit.mEffectiveDirective = aEffectiveDirective; + + // violated-directive + // In CSP2, the policy directive that was violated, as it appears in the + // policy. In CSP3, the same as effective-directive. + aViolationEventInit.mViolatedDirective = aEffectiveDirective; + + // original-policy + nsAutoString originalPolicy; + rv = this->GetPolicyString(aViolatedPolicyIndex, originalPolicy); + NS_ENSURE_SUCCESS(rv, rv); + aViolationEventInit.mOriginalPolicy = originalPolicy; + + // 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; + StripURIForReporting(mSelfURI, sourceURI, aEffectiveDirective, spec); + CopyUTF8toUTF16(spec, aViolationEventInit.mSourceFile); + } else { + aViolationEventInit.mSourceFile = aSourceFile; + } + } + + // sample (already truncated) + aViolationEventInit.mSample = aScriptSample; + + // disposition + aViolationEventInit.mDisposition = + mPolicies[aViolatedPolicyIndex]->getReportOnlyFlag() + ? mozilla::dom::SecurityPolicyViolationEventDisposition::Report + : mozilla::dom::SecurityPolicyViolationEventDisposition::Enforce; + + // status-code + uint16_t statusCode = 0; + { + nsCOMPtr<Document> doc = do_QueryReferent(mLoadingContext); + if (doc) { + nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(doc->GetChannel()); + if (channel) { + uint32_t responseStatus = 0; + nsresult rv = channel->GetResponseStatus(&responseStatus); + if (NS_SUCCEEDED(rv) && (responseStatus <= UINT16_MAX)) { + statusCode = static_cast<uint16_t>(responseStatus); + } + } + } + } + aViolationEventInit.mStatusCode = statusCode; + + // line-number + aViolationEventInit.mLineNumber = aLineNum; + + // column-number + aViolationEventInit.mColumnNumber = aColumnNum; + + aViolationEventInit.mBubbles = true; + aViolationEventInit.mComposed = true; + + return NS_OK; +} + +nsresult nsCSPContext::SendReports( + const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit, + uint32_t aViolatedPolicyIndex) { + EnsureIPCPoliciesRead(); + NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1); + + dom::CSPReport report; + + // blocked-uri + report.mCsp_report.mBlocked_uri = aViolationEventInit.mBlockedURI; + + // document-uri + report.mCsp_report.mDocument_uri = aViolationEventInit.mDocumentURI; + + // original-policy + report.mCsp_report.mOriginal_policy = aViolationEventInit.mOriginalPolicy; + + // referrer + report.mCsp_report.mReferrer = aViolationEventInit.mReferrer; + + // effective-directive + report.mCsp_report.mEffective_directive = + aViolationEventInit.mEffectiveDirective; + + // violated-directive + report.mCsp_report.mViolated_directive = + aViolationEventInit.mEffectiveDirective; + + // disposition + report.mCsp_report.mDisposition = aViolationEventInit.mDisposition; + + // status-code + report.mCsp_report.mStatus_code = aViolationEventInit.mStatusCode; + + // source-file + if (!aViolationEventInit.mSourceFile.IsEmpty()) { + report.mCsp_report.mSource_file.Construct(); + report.mCsp_report.mSource_file.Value() = aViolationEventInit.mSourceFile; + } + + // script-sample + if (!aViolationEventInit.mSample.IsEmpty()) { + report.mCsp_report.mScript_sample.Construct(); + report.mCsp_report.mScript_sample.Value() = aViolationEventInit.mSample; + } + + // line-number + if (aViolationEventInit.mLineNumber != 0) { + report.mCsp_report.mLine_number.Construct(); + report.mCsp_report.mLine_number.Value() = aViolationEventInit.mLineNumber; + } + + if (aViolationEventInit.mColumnNumber != 0) { + report.mCsp_report.mColumn_number.Construct(); + report.mCsp_report.mColumn_number.Value() = + aViolationEventInit.mColumnNumber; + } + + 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<Document> doc = do_QueryReferent(mLoadingContext); + nsCOMPtr<nsIURI> reportURI; + nsCOMPtr<nsIChannel> reportChannel; + + nsresult rv; + 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)) { + AutoTArray<nsString, 1> params = {reportURIs[r]}; + CSPCONTEXTLOG(("Could not create nsIURI for report URI %s", + reportURICstring.get())); + logToConsole("triedToSendReport", params, aViolationEventInit.mSourceFile, + aViolationEventInit.mSample, aViolationEventInit.mLineNumber, + aViolationEventInit.mColumnNumber, + nsIScriptError::errorFlag); + continue; // don't return yet, there may be more URIs + } + + // try to create a new channel for every report-uri + if (doc) { + rv = + NS_NewChannel(getter_AddRefs(reportChannel), reportURI, doc, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_CSP_REPORT); + } else { + rv = NS_NewChannel( + getter_AddRefs(reportChannel), reportURI, mLoadingPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_CSP_REPORT); + } + + 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 = + reportURI->SchemeIs("http") || reportURI->SchemeIs("https"); + + if (!isHttpScheme) { + AutoTArray<nsString, 1> params = {reportURIs[r]}; + logToConsole( + "reportURInotHttpsOrHttp2", params, aViolationEventInit.mSourceFile, + aViolationEventInit.mSample, aViolationEventInit.mLineNumber, + aViolationEventInit.mColumnNumber, 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 taken by setRequestContextWithDocument. If there's + // no loadgroup, AsyncOpen will fail on process-split necko (since the + // channel cannot query the iBrowserChild). + 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, "application/csp-report"_ns, -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) { + rv = httpChannel->SetRequestMethod("POST"_ns); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + RefPtr<CSPViolationReportListener> listener = + new CSPViolationReportListener(); + rv = reportChannel->AsyncOpen(listener); + + // AsyncOpen should not fail, but could if there's no load group (like if + // SetRequestContextWith{Document,Principal} 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)) { + AutoTArray<nsString, 1> params = {reportURIs[r]}; + CSPCONTEXTLOG(("AsyncOpen failed for report URI %s", + NS_ConvertUTF16toUTF8(params[0]).get())); + logToConsole("triedToSendReport", params, aViolationEventInit.mSourceFile, + aViolationEventInit.mSample, aViolationEventInit.mLineNumber, + aViolationEventInit.mColumnNumber, + nsIScriptError::errorFlag); + } else { + CSPCONTEXTLOG( + ("Sent violation report to URI %s", reportURICstring.get())); + } + } + return NS_OK; +} + +nsresult nsCSPContext::FireViolationEvent( + Element* aTriggeringElement, nsICSPEventListener* aCSPEventListener, + const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit) { + if (aCSPEventListener) { + nsAutoString json; + if (aViolationEventInit.ToJSON(json)) { + aCSPEventListener->OnCSPViolationEvent(json); + } + } + + // 1. If target is not null, and global is a Window, and target’s + // shadow-including root is not global’s associated Document, set target to + // null. + RefPtr<EventTarget> eventTarget = aTriggeringElement; + + nsCOMPtr<Document> doc = do_QueryReferent(mLoadingContext); + if (doc && aTriggeringElement && + aTriggeringElement->GetComposedDoc() != doc) { + eventTarget = nullptr; + } + + if (!eventTarget) { + // If target is a Window, set target to target’s associated Document. + eventTarget = doc; + } + + if (!eventTarget && mInnerWindowID && XRE_IsParentProcess()) { + if (RefPtr<WindowGlobalParent> parent = + WindowGlobalParent::GetByInnerWindowId(mInnerWindowID)) { + nsAutoString json; + if (aViolationEventInit.ToJSON(json)) { + Unused << parent->SendDispatchSecurityPolicyViolation(json); + } + } + return NS_OK; + } + + if (!eventTarget) { + // If we are here, we are probably dealing with workers. Those are handled + // via nsICSPEventListener. Nothing to do here. + return NS_OK; + } + + RefPtr<mozilla::dom::Event> event = + mozilla::dom::SecurityPolicyViolationEvent::Constructor( + eventTarget, u"securitypolicyviolation"_ns, aViolationEventInit); + event->SetTrusted(true); + + ErrorResult rv; + eventTarget->DispatchEvent(*event, rv); + return rv.StealNSResult(); +} + +/** + * Dispatched from the main thread to send reports for one CSP violation. + */ +class CSPReportSenderRunnable final : public Runnable { + public: + CSPReportSenderRunnable( + Element* aTriggeringElement, nsICSPEventListener* aCSPEventListener, + nsIURI* aBlockedURI, + nsCSPContext::BlockedContentSource aBlockedContentSource, + nsIURI* aOriginalURI, uint32_t aViolatedPolicyIndex, bool aReportOnlyFlag, + const nsAString& aViolatedDirective, const nsAString& aEffectiveDirective, + const nsAString& aObserverSubject, const nsAString& aSourceFile, + bool aReportSample, const nsAString& aScriptSample, uint32_t aLineNum, + uint32_t aColumnNum, nsCSPContext* aCSPContext) + : mozilla::Runnable("CSPReportSenderRunnable"), + mTriggeringElement(aTriggeringElement), + mCSPEventListener(aCSPEventListener), + mBlockedURI(aBlockedURI), + mBlockedContentSource(aBlockedContentSource), + mOriginalURI(aOriginalURI), + mViolatedPolicyIndex(aViolatedPolicyIndex), + mReportOnlyFlag(aReportOnlyFlag), + mReportSample(aReportSample), + mViolatedDirective(aViolatedDirective), + mEffectiveDirective(aEffectiveDirective), + mSourceFile(aSourceFile), + mScriptSample(aScriptSample), + mLineNum(aLineNum), + mColumnNum(aColumnNum), + 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() && mBlockedURI) { + mObserverSubject = aBlockedURI; + return; + } + + nsAutoCString subject; + if (aObserverSubject.IsEmpty()) { + BlockedContentSourceToString(aBlockedContentSource, subject); + } else { + CopyUTF16toUTF8(aObserverSubject, subject); + } + + nsCOMPtr<nsISupportsCString> supportscstr = + do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID); + if (supportscstr) { + supportscstr->SetData(subject); + mObserverSubject = do_QueryInterface(supportscstr); + } + + // Truncate sample string. + uint32_t length = mScriptSample.Length(); + if (length > nsCSPContext::ScriptSampleMaxLength()) { + uint32_t desiredLength = nsCSPContext::ScriptSampleMaxLength(); + // Don't cut off right before a low surrogate. Just include it. + if (NS_IS_LOW_SURROGATE(mScriptSample[desiredLength])) { + desiredLength++; + } + mScriptSample.Replace(nsCSPContext::ScriptSampleMaxLength(), + length - desiredLength, + nsContentUtils::GetLocalizedEllipsis()); + } + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv; + + // 0) prepare violation data + mozilla::dom::SecurityPolicyViolationEventInit init; + + nsAutoCString blockedContentSource; + BlockedContentSourceToString(mBlockedContentSource, blockedContentSource); + + rv = mCSPContext->GatherSecurityPolicyViolationEventData( + mBlockedURI, blockedContentSource, mOriginalURI, mEffectiveDirective, + mViolatedPolicyIndex, mSourceFile, + mReportSample ? mScriptSample : EmptyString(), mLineNum, mColumnNum, + init); + NS_ENSURE_SUCCESS(rv, rv); + + // 1) notify observers + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (mObserverSubject && observerService) { + 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(init, mViolatedPolicyIndex); + + // 3) log to console (one per policy violation) + + if (mBlockedURI) { + mBlockedURI->GetSpec(blockedContentSource); + if (blockedContentSource.Length() > + nsCSPContext::ScriptSampleMaxLength()) { + bool isData = mBlockedURI->SchemeIs("data"); + if (NS_SUCCEEDED(rv) && isData && + blockedContentSource.Length() > + nsCSPContext::ScriptSampleMaxLength()) { + blockedContentSource.Truncate(nsCSPContext::ScriptSampleMaxLength()); + blockedContentSource.Append( + NS_ConvertUTF16toUTF8(nsContentUtils::GetLocalizedEllipsis())); + } + } + } + + if (blockedContentSource.Length() > 0) { + nsString blockedContentSource16 = + NS_ConvertUTF8toUTF16(blockedContentSource); + AutoTArray<nsString, 2> params = {mViolatedDirective, + blockedContentSource16}; + mCSPContext->logToConsole( + mReportOnlyFlag ? "CSPROViolationWithURI" : "CSPViolationWithURI", + params, mSourceFile, mScriptSample, mLineNum, mColumnNum, + nsIScriptError::errorFlag); + } + + // 4) fire violation event + // A frame-ancestors violation has occurred, but we should not dispatch + // the violation event to a potentially cross-origin ancestor. + if (!mViolatedDirective.EqualsLiteral("frame-ancestors")) { + mCSPContext->FireViolationEvent(mTriggeringElement, mCSPEventListener, + init); + } + + return NS_OK; + } + + private: + RefPtr<Element> mTriggeringElement; + nsCOMPtr<nsICSPEventListener> mCSPEventListener; + nsCOMPtr<nsIURI> mBlockedURI; + nsCSPContext::BlockedContentSource mBlockedContentSource; + nsCOMPtr<nsIURI> mOriginalURI; + uint32_t mViolatedPolicyIndex; + bool mReportOnlyFlag; + bool mReportSample; + nsString mViolatedDirective; + nsString mEffectiveDirective; + nsCOMPtr<nsISupports> mObserverSubject; + nsString mSourceFile; + nsString mScriptSample; + uint32_t mLineNum; + uint32_t mColumnNum; + 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 aTriggeringElement + * The element that triggered this report violation. It can be null. + * @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) + * @param aColumnNum + * source column number of the violation (if available) + */ +nsresult nsCSPContext::AsyncReportViolation( + Element* aTriggeringElement, nsICSPEventListener* aCSPEventListener, + nsIURI* aBlockedURI, BlockedContentSource aBlockedContentSource, + nsIURI* aOriginalURI, const nsAString& aViolatedDirective, + const nsAString& aEffectiveDirective, uint32_t aViolatedPolicyIndex, + const nsAString& aObserverSubject, const nsAString& aSourceFile, + bool aReportSample, const nsAString& aScriptSample, uint32_t aLineNum, + uint32_t aColumnNum) { + EnsureIPCPoliciesRead(); + NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1); + + nsCOMPtr<nsIRunnable> task = new CSPReportSenderRunnable( + aTriggeringElement, aCSPEventListener, aBlockedURI, aBlockedContentSource, + aOriginalURI, aViolatedPolicyIndex, + mPolicies[aViolatedPolicyIndex]->getReportOnlyFlag(), aViolatedDirective, + aEffectiveDirective, aObserverSubject, aSourceFile, aReportSample, + aScriptSample, aLineNum, aColumnNum, this); + + if (XRE_IsContentProcess()) { + if (mEventTarget) { + mEventTarget->Dispatch(task.forget(), NS_DISPATCH_NORMAL); + return NS_OK; + } + } + + NS_DispatchToMainThread(task.forget()); + return NS_OK; +} + +/** + * Based on the given loadinfo, 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 traverses all Browsing Contexts + * until it reaches the top level browsing context. + */ +NS_IMETHODIMP +nsCSPContext::PermitsAncestry(nsILoadInfo* aLoadInfo, + bool* outPermitsAncestry) { + nsresult rv; + + *outPermitsAncestry = true; + + RefPtr<mozilla::dom::BrowsingContext> ctx; + aLoadInfo->GetBrowsingContext(getter_AddRefs(ctx)); + + // extract the ancestry as an array + nsCOMArray<nsIURI> ancestorsArray; + nsCOMPtr<nsIURI> uriClone; + + while (ctx) { + nsCOMPtr<nsIPrincipal> currentPrincipal; + // Generally permitsAncestry is consulted from within the + // DocumentLoadListener in the parent process. For loads of type object + // and embed it's called from the Document in the content process. + // After Bug 1646899 we should be able to remove that branching code for + // querying the currentURI. + if (XRE_IsParentProcess()) { + WindowGlobalParent* window = ctx->Canonical()->GetCurrentWindowGlobal(); + if (window) { + // Using the URI of the Principal and not the document because e.g. + // about:blank inherits the principal and hence the URI of the + // document does not reflect the security context of the document. + currentPrincipal = window->DocumentPrincipal(); + } + } else if (nsPIDOMWindowOuter* windowOuter = ctx->GetDOMWindow()) { + currentPrincipal = nsGlobalWindowOuter::Cast(windowOuter)->GetPrincipal(); + } + + if (currentPrincipal) { + nsCOMPtr<nsIURI> currentURI; + auto* currentBasePrincipal = BasePrincipal::Cast(currentPrincipal); + currentBasePrincipal->GetURI(getter_AddRefs(currentURI)); + + if (currentURI) { + nsAutoCString spec; + currentURI->GetSpec(spec); + // delete the userpass from the URI. + rv = NS_MutateURI(currentURI) + .SetRef(""_ns) + .SetUserPass(""_ns) + .Finalize(uriClone); + + // If setUserPass fails for some reason, just return a clone of the + // current URI + if (NS_FAILED(rv)) { + rv = NS_GetURIWithoutRef(currentURI, getter_AddRefs(uriClone)); + NS_ENSURE_SUCCESS(rv, rv); + } + ancestorsArray.AppendElement(uriClone); + } + } + ctx = ctx->GetParent(); + } + + 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, + nullptr, // triggering element + nullptr, // nsICSPEventListener + ancestorsArray[a], + nullptr, // no redirect here. + u""_ns, // no nonce + 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(Element* aTriggeringElement, + nsICSPEventListener* aCSPEventListener, nsIURI* aURI, + CSPDirective aDir, bool aSpecific, + bool aSendViolationReports, bool* outPermits) { + // Can't perform check without aURI + if (aURI == nullptr) { + return NS_ERROR_FAILURE; + } + + if (aURI->SchemeIs("resource")) { + // XXX Ideally we would call SubjectToCSP() here but that would also + // allowlist e.g. javascript: URIs which should not be allowlisted here. + // As a hotfix we just allowlist pdf.js internals here explicitly. + nsAutoCString uriSpec; + aURI->GetSpec(uriSpec); + if (StringBeginsWith(uriSpec, "resource://pdf.js/"_ns)) { + *outPermits = true; + return NS_OK; + } + } + + *outPermits = + permitsInternal(aDir, aTriggeringElement, aCSPEventListener, aURI, + nullptr, // no original (pre-redirect) URI + u""_ns, // no nonce + aSpecific, aSendViolationReports, + true, // send blocked URI in violation reports + false); // not parser created + + if (CSPCONTEXTLOGENABLED()) { + CSPCONTEXTLOG(("nsCSPContext::Permits, aUri: %s, aDir: %s, isAllowed: %s", + aURI->GetSpecOrDefault().get(), + CSP_CSPDirectiveToString(aDir), + *outPermits ? "allow" : "deny")); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCSPContext::ToJSON(nsAString& outCSPinJSON) { + outCSPinJSON.Truncate(); + dom::CSPPolicies jsonPolicies; + jsonPolicies.mCsp_policies.Construct(); + EnsureIPCPoliciesRead(); + + for (uint32_t p = 0; p < mPolicies.Length(); p++) { + dom::CSP jsonCSP; + mPolicies[p]->toDomCSPStruct(jsonCSP); + if (!jsonPolicies.mCsp_policies.Value().AppendElement(jsonCSP, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + // 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; + + EnsureIPCPoliciesRead(); + 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", + NS_ConvertUTF16toUTF8(policy).get())); + + AutoTArray<nsString, 1> params = {policy}; + logToConsole("ignoringReportOnlyDirective", params, u""_ns, u""_ns, 0, 0, + nsIScriptError::warningFlag); + } + } + + return NS_OK; +} + +/* ========== CSPViolationReportListener implementation ========== */ + +NS_IMPL_ISUPPORTS(CSPViolationReportListener, nsIStreamListener, + nsIRequestObserver, nsISupports); + +CSPViolationReportListener::CSPViolationReportListener() = default; + +CSPViolationReportListener::~CSPViolationReportListener() = default; + +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, + 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, + nsresult aStatus) { + return NS_OK; +} + +NS_IMETHODIMP +CSPViolationReportListener::OnStartRequest(nsIRequest* aRequest) { + return NS_OK; +} + +/* ========== CSPReportRedirectSink implementation ========== */ + +NS_IMPL_ISUPPORTS(CSPReportRedirectSink, nsIChannelEventSink, + nsIInterfaceRequestor); + +CSPReportRedirectSink::CSPReportRedirectSink() = default; + +CSPReportRedirectSink::~CSPReportRedirectSink() = default; + +NS_IMETHODIMP +CSPReportRedirectSink::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aRedirFlags, + nsIAsyncVerifyRedirectCallback* aCallback) { + if (aRedirFlags & nsIChannelEventSink::REDIRECT_INTERNAL) { + aCallback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; + } + + // 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); + MOZ_ASSERT(mSelfURI, "need a self URI to de-serialize"); + + nsAutoCString JSON; + rv = aStream->ReadCString(JSON); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrincipal> principal = BasePrincipal::FromJSON(JSON); + mLoadingPrincipal = principal; + MOZ_ASSERT(mLoadingPrincipal, "need a loadingPrincipal 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); + + bool deliveredViaMetaTag = false; + rv = aStream->ReadBoolean(&deliveredViaMetaTag); + NS_ENSURE_SUCCESS(rv, rv); + AddIPCPolicy(mozilla::ipc::ContentSecurityPolicy(policyString, reportOnly, + deliveredViaMetaTag)); + } + + 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); + + nsAutoCString JSON; + BasePrincipal::Cast(mLoadingPrincipal)->ToJSON(JSON); + rv = aStream->WriteStringZ(JSON.get()); + NS_ENSURE_SUCCESS(rv, rv); + + // Serialize all the policies. + aStream->Write32(mPolicies.Length() + mIPCPolicies.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()); + aStream->WriteBoolean(mPolicies[p]->getDeliveredViaMetaTagFlag()); + } + for (auto& policy : mIPCPolicies) { + aStream->WriteWStringZ(policy.policy().get()); + aStream->WriteBoolean(policy.reportOnlyFlag()); + aStream->WriteBoolean(policy.deliveredViaMetaTagFlag()); + } + return NS_OK; +} + +void nsCSPContext::AddIPCPolicy(const ContentSecurityPolicy& aPolicy) { + mIPCPolicies.AppendElement(aPolicy); +} + +void nsCSPContext::SerializePolicies( + nsTArray<ContentSecurityPolicy>& aPolicies) { + for (auto* policy : mPolicies) { + nsAutoString policyString; + policy->toString(policyString); + aPolicies.AppendElement( + ContentSecurityPolicy(policyString, policy->getReportOnlyFlag(), + policy->getDeliveredViaMetaTagFlag())); + } + + aPolicies.AppendElements(mIPCPolicies); +} diff --git a/dom/security/nsCSPContext.h b/dom/security/nsCSPContext.h new file mode 100644 index 0000000000..85d286f901 --- /dev/null +++ b/dom/security/nsCSPContext.h @@ -0,0 +1,235 @@ +/* -*- 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 "mozilla/dom/SecurityPolicyViolationEvent.h" +#include "mozilla/StaticPrefs_security.h" +#include "nsIChannel.h" +#include "nsIChannelEventSink.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIInterfaceRequestor.h" +#include "nsIStreamListener.h" +#include "nsIWeakReferenceUtils.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; +class nsIEventTarget; +struct ConsoleMsgQueueElem; + +namespace mozilla { +namespace dom { +class Element; +} +namespace ipc { +class ContentSecurityPolicy; +} +} // namespace mozilla + +class nsCSPContext : public nsIContentSecurityPolicy { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTSECURITYPOLICY + NS_DECL_NSISERIALIZABLE + + protected: + virtual ~nsCSPContext(); + + public: + nsCSPContext(); + + static bool Equals(nsIContentSecurityPolicy* aCSP, + nsIContentSecurityPolicy* aOtherCSP); + + // Init a CSP from a different CSP + nsresult InitFromOther(nsCSPContext* otherContext); + + // Used to suppress errors and warnings produced by the parser. + // Use this when doing an one-off parsing of the CSP. + void SuppressParserLogMessages() { mSuppressParserLogMessages = true; } + + /** + * SetRequestContextWithDocument() 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 initialize + * the innerWindowID. Node, If SetRequestContextWithPrincipal() was + * called then we do not have a innerWindowID anyway and hence + * we can not flush messages to the correct console. + */ + void flushConsoleMessages(); + + void logToConsole(const char* aName, const nsTArray<nsString>& aParams, + const nsAString& aSourceName, const nsAString& aSourceLine, + uint32_t aLineNumber, uint32_t aColumnNumber, + uint32_t aSeverityFlag); + + /** + * Construct SecurityPolicyViolationEventInit structure. + * + * @param aBlockedURI + * A 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) + * @param aColumnNum + * source column number of the violation (if available) + * @param aViolationEventInit + * The output + */ + nsresult GatherSecurityPolicyViolationEventData( + nsIURI* aBlockedURI, const nsACString& aBlockedString, + nsIURI* aOriginalURI, const nsAString& aViolatedDirective, + uint32_t aViolatedPolicyIndex, const nsAString& aSourceFile, + const nsAString& aScriptSample, uint32_t aLineNum, uint32_t aColumnNum, + mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit); + + nsresult SendReports( + const mozilla::dom::SecurityPolicyViolationEventInit& aViolationEventInit, + uint32_t aViolatedPolicyIndex); + + nsresult FireViolationEvent( + mozilla::dom::Element* aTriggeringElement, + nsICSPEventListener* aCSPEventListener, + const mozilla::dom::SecurityPolicyViolationEventInit& + aViolationEventInit); + + enum BlockedContentSource { + eUnknown, + eInline, + eEval, + eSelf, + eWasmEval, + }; + + nsresult AsyncReportViolation( + mozilla::dom::Element* aTriggeringElement, + nsICSPEventListener* aCSPEventListener, nsIURI* aBlockedURI, + BlockedContentSource aBlockedContentSource, nsIURI* aOriginalURI, + const nsAString& aViolatedDirective, const nsAString& aEffectiveDirective, + uint32_t aViolatedPolicyIndex, const nsAString& aObserverSubject, + const nsAString& aSourceFile, bool aReportSample, + const nsAString& aScriptSample, uint32_t aLineNum, uint32_t aColumnNum); + + // 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; } + + static uint32_t ScriptSampleMaxLength() { + return std::max( + mozilla::StaticPrefs::security_csp_reporting_script_sample_max_length(), + 0); + } + + void AddIPCPolicy(const mozilla::ipc::ContentSecurityPolicy& aPolicy); + void SerializePolicies( + nsTArray<mozilla::ipc::ContentSecurityPolicy>& aPolicies); + + private: + void EnsureIPCPoliciesRead(); + + bool permitsInternal(CSPDirective aDir, + mozilla::dom::Element* aTriggeringElement, + nsICSPEventListener* aCSPEventListener, + nsIURI* aContentLocation, nsIURI* aOriginalURIIfRedirect, + const nsAString& aNonce, bool aSpecific, + bool aSendViolationReports, + bool aSendContentLocationInViolationReports, + bool aParserCreated); + + // helper to report inline script/style violations + void reportInlineViolation(CSPDirective aDirective, + mozilla::dom::Element* aTriggeringElement, + nsICSPEventListener* aCSPEventListener, + const nsAString& aNonce, bool aReportSample, + const nsAString& aSample, + const nsAString& aViolatedDirective, + const nsAString& aEffectiveDirective, + uint32_t aViolatedPolicyIndex, + uint32_t aLineNumber, uint32_t aColumnNumber); + + nsString mReferrer; + uint64_t mInnerWindowID; // used for web console logging + bool mSkipAllowInlineStyleCheck; // used to allow Devtools to edit styles + // When deserializing an nsCSPContext instance, we initially just keep the + // policies unparsed. We will only reconstruct actual CSP policy instances + // when there's an attempt to use the CSP. Given a better way to serialize/ + // deserialize individual nsCSPPolicy objects, this performance + // optimization could go away. + nsTArray<mozilla::ipc::ContentSecurityPolicy> mIPCPolicies; + nsTArray<nsCSPPolicy*> mPolicies; + nsCOMPtr<nsIURI> mSelfURI; + nsCOMPtr<nsILoadGroup> mCallingChannelLoadGroup; + nsWeakPtr mLoadingContext; + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + + bool mSuppressParserLogMessages = false; + + // helper members used to queue up web console messages till + // the windowID becomes available. see flushConsoleMessages() + nsTArray<ConsoleMsgQueueElem> mConsoleMsgQueue; + bool mQueueUpMessages; + nsCOMPtr<nsIEventTarget> mEventTarget; +}; + +// 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 0000000000..623de34f28 --- /dev/null +++ b/dom/security/nsCSPParser.cpp @@ -0,0 +1,1290 @@ +/* -*- 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/TextUtils.h" +#include "mozilla/dom/Document.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_security.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsCSPParser.h" +#include "nsCSPUtils.h" +#include "nsIScriptError.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsUnicharUtils.h" + +using namespace mozilla; +using namespace mozilla::dom; + +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 uint32_t kSubHostPathCharacterCutoff = 512; + +static const char* const kHashSourceValidFns[] = {"sha256", "sha384", "sha512"}; +static const uint32_t kHashSourceValidFnsLen = 3; + +/* ===== nsCSPParser ==================== */ + +nsCSPParser::nsCSPParser(policyTokens& aTokens, nsIURI* aSelfURI, + nsCSPContext* aCSPContext, bool aDeliveredViaMetaTag, + bool aSuppressLogMessages) + : mCurChar(nullptr), + mEndChar(nullptr), + mHasHashOrNonce(false), + mHasAnyUnsafeEval(false), + mStrictDynamic(false), + mUnsafeInlineKeywordSrc(nullptr), + mChildSrc(nullptr), + mFrameSrc(nullptr), + mWorkerSrc(nullptr), + mScriptSrc(nullptr), + mStyleSrc(nullptr), + mParsingFrameAncestorsDir(false), + mTokens(aTokens.Clone()), + mSelfURI(aSelfURI), + mPolicy(nullptr), + mCSPContext(aCSPContext), + mDeliveredViaMetaTag(aDeliveredViaMetaTag), + mSuppressLogMessages(aSuppressLogMessages) { + CSPPARSERLOG(("nsCSPParser::nsCSPParser")); +} + +nsCSPParser::~nsCSPParser() { CSPPARSERLOG(("nsCSPParser::~nsCSPParser")); } + +static bool isCharacterToken(char16_t aSymbol) { + return (aSymbol >= 'a' && aSymbol <= 'z') || + (aSymbol >= 'A' && aSymbol <= 'Z'); +} + +bool isNumberToken(char16_t aSymbol) { + return (aSymbol >= '0' && aSymbol <= '9'); +} + +bool isValidHexDig(char16_t aHexDig) { + return (isNumberToken(aHexDig) || (aHexDig >= 'A' && aHexDig <= 'F') || + (aHexDig >= 'a' && aHexDig <= 'f')); +} + +static bool isValidBase64Value(const char16_t* cur, const char16_t* end) { + // Using grammar at + // https://w3c.github.io/webappsec-csp/#grammardef-nonce-source + + // May end with one or two = + if (end > cur && *(end - 1) == EQUALS) end--; + if (end > cur && *(end - 1) == EQUALS) end--; + + // Must have at least one character aside from any = + if (end == cur) { + return false; + } + + // Rest must all be A-Za-z0-9+/-_ + for (; cur < end; ++cur) { + if (!(isCharacterToken(*cur) || isNumberToken(*cur) || *cur == PLUS || + *cur == SLASH || *cur == DASH || *cur == UNDERLINE)) { + return false; + } + } + + return true; +} + +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 nsTArray<nsString>& aParams) { + CSPPARSERLOG(("nsCSPParser::logWarningErrorToConsole: %s", aProperty)); + + if (mSuppressLogMessages) { + return; + } + + // send console messages off to the context and let the context + // deal with it (potentially messages need to be queued up) + mCSPContext->logToConsole(aProperty, aParams, + u""_ns, // aSourceName + u""_ns, // 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)) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, "couldntParsePort", + 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()) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "couldntParseInvalidSource", 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)) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "couldntParseInvalidSource", 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(u"/"_ns); + return true; + } + // path can begin with "/" but not "//" + // see http://tools.ietf.org/html/rfc3986#section-3.3 + if (peek(SLASH)) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "couldntParseInvalidSource", 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)) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "couldntParseInvalidHost", params); + return nullptr; + } + } + + // Expecting at least one host-char + if (!hostChar()) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "couldntParseInvalidHost", params); + return nullptr; + } + + // There might be several sub hosts defined. + if (!subHost()) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "couldntParseInvalidHost", params); + return nullptr; + } + + // HostName might match a keyword, log to the console. + if (CSP_IsQuotelessKeyword(mCurValue)) { + nsString keyword = mCurValue; + ToLowerCase(keyword); + AutoTArray<nsString, 2> params = {mCurToken, keyword}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "hostNameMightBeKeyword", params); + } + + // Create a new nsCSPHostSrc with the parsed host. + return new nsCSPHostSrc(mCurValue); +} + +// keyword-source = "'self'" / "'unsafe-inline'" / "'unsafe-eval'" / +// "'wasm-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_CreateHostSrcFromSelfURI(mSelfURI); + } + + if (CSP_IsKeyword(mCurToken, CSP_REPORT_SAMPLE)) { + return new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(mCurToken)); + } + + if (CSP_IsKeyword(mCurToken, CSP_STRICT_DYNAMIC)) { + if (!CSP_IsDirective(mCurDir[0], + nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) && + !CSP_IsDirective(mCurDir[0], + nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) && + !CSP_IsDirective(mCurDir[0], + nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE)) { + // Todo: Enforce 'strict-dynamic' within default-src; see Bug 1313937 + AutoTArray<nsString, 1> params = {u"strict-dynamic"_ns}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "ignoringStrictDynamic", params); + return nullptr; + } + mStrictDynamic = true; + return new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(mCurToken)); + } + + if (CSP_IsKeyword(mCurToken, CSP_UNSAFE_INLINE)) { + nsWeakPtr ctx = mCSPContext->GetLoadingContext(); + nsCOMPtr<Document> 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) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "ignoringDuplicateSrc", 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_UTF16KeywordToEnum(mCurToken)); + return mUnsafeInlineKeywordSrc; + } + + if (CSP_IsKeyword(mCurToken, CSP_UNSAFE_EVAL)) { + nsWeakPtr ctx = mCSPContext->GetLoadingContext(); + nsCOMPtr<Document> doc = do_QueryReferent(ctx); + if (doc) { + doc->SetHasUnsafeEvalCSP(true); + } + mHasAnyUnsafeEval = true; + return new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(mCurToken)); + } + + if (StaticPrefs::security_csp_wasm_unsafe_eval_enabled() && + CSP_IsKeyword(mCurToken, CSP_WASM_UNSAFE_EVAL)) { + mHasAnyUnsafeEval = true; + return new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(mCurToken)); + } + + if (StaticPrefs::security_csp_unsafe_hashes_enabled() && + CSP_IsKeyword(mCurToken, CSP_UNSAFE_HASHES)) { + return new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(mCurToken)); + } + + if (CSP_IsKeyword(mCurToken, CSP_UNSAFE_ALLOW_REDIRECTS)) { + if (!CSP_IsDirective(mCurDir[0], + nsIContentSecurityPolicy::NAVIGATE_TO_DIRECTIVE)) { + // Only allow 'unsafe-allow-redirects' within navigate-to. + AutoTArray<nsString, 2> params = {u"unsafe-allow-redirects"_ns, + u"navigate-to"_ns}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "IgnoringSourceWithinDirective", params); + return nullptr; + } + + return new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(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())); + + 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, + nsDependentString(CSP_EnumToUTF16Keyword(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; + } + if (!isValidBase64Value(expr.BeginReading() + dashIndex + 1, + expr.EndReading())) { + 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; + } + + if (!isValidBase64Value(expr.BeginReading() + dashIndex + 1, + expr.EndReading())) { + 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(u"*"_ns); + } + + // 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)) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "failedToParseUnrecognizedSource", 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); + cspHost->setWithinFrameAncestorsDir(mParsingFrameAncestorsDir); + 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.IsEmpty() || + (outSrcs.Length() == 1 && outSrcs[0]->isReportSample())) { + nsCSPKeywordSrc* keyword = new nsCSPKeywordSrc(CSP_NONE); + outSrcs.InsertElementAt(0, keyword); + } + // Otherwise, we ignore 'none' and report a warning + else { + AutoTArray<nsString, 1> params; + params.AppendElement(CSP_EnumToUTF16Keyword(CSP_NONE)); + logWarningErrorToConsole(nsIScriptError::warningFlag, + "ignoringUnknownOption", params); + } + } +} + +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)) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "couldNotParseReportURI", params); + continue; + } + + // Create new nsCSPReportURI and append to the list. + nsCSPReportURI* reportURI = new nsCSPReportURI(uri); + srcs.AppendElement(reportURI); + } + + if (srcs.Length() == 0) { + AutoTArray<nsString, 1> directiveName = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "ignoringDirectiveWithNoValues", 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)) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "couldntParseInvalidSandboxFlag", params); + continue; + } + + flags.Append(mCurToken); + if (i != mCurDir.Length() - 1) { + flags.AppendLiteral(" "); + } + } + + // 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 + CSPDirective directive = CSP_StringToCSPDirective(mCurToken); + if (directive == nsIContentSecurityPolicy::NO_DIRECTIVE) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "couldNotProcessUnknownDirective", 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 (directive == nsIContentSecurityPolicy::REFLECTED_XSS_DIRECTIVE) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "notSupportingDirective", params); + return nullptr; + } + + // script-src-attr and script-scr-elem might have been disabled. + // Similarly style-src-{attr, elem}. + if (((directive == nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE || + directive == nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) && + !StaticPrefs::security_csp_script_src_attr_elem_enabled()) || + ((directive == nsIContentSecurityPolicy::STYLE_SRC_ATTR_DIRECTIVE || + directive == nsIContentSecurityPolicy::STYLE_SRC_ELEM_DIRECTIVE) && + !StaticPrefs::security_csp_style_src_attr_elem_enabled())) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "notSupportingDirective", params); + return nullptr; + } + + // Bug 1529068: Implement navigate-to directive. + // Once all corner cases are resolved we can remove that special + // if-handling here and let the parser just fall through to + // return new nsCSPDirective. + if (directive == nsIContentSecurityPolicy::NAVIGATE_TO_DIRECTIVE && + !StaticPrefs::security_csp_enableNavigateTo()) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "couldNotProcessUnknownDirective", params); + return nullptr; + } + + // Make sure the directive does not already exist + // (see http://www.w3.org/TR/CSP11/#parsing) + if (mPolicy->hasDirective(directive)) { + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, "duplicateDirective", + 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 && + ((directive == nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE) || + (directive == nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE) || + (directive == nsIContentSecurityPolicy::SANDBOX_DIRECTIVE))) { + // log to the console to indicate that meta CSP is ignoring the directive + AutoTArray<nsString, 1> params = {mCurToken}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "ignoringSrcFromMetaCSP", params); + return nullptr; + } + + // special case handling for block-all-mixed-content + if (directive == nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT) { + return new nsBlockAllMixedContentDirective(directive); + } + + // special case handling for upgrade-insecure-requests + if (directive == nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE) { + return new nsUpgradeInsecureDirective(directive); + } + + // if we have a child-src, cache it as a fallback for + // * workers (if worker-src is not explicitly specified) + // * frames (if frame-src is not explicitly specified) + if (directive == nsIContentSecurityPolicy::CHILD_SRC_DIRECTIVE) { + mChildSrc = new nsCSPChildSrcDirective(directive); + return mChildSrc; + } + + // if we have a frame-src, cache it so we can discard child-src for frames + if (directive == nsIContentSecurityPolicy::FRAME_SRC_DIRECTIVE) { + mFrameSrc = new nsCSPDirective(directive); + return mFrameSrc; + } + + // if we have a worker-src, cache it so we can discard child-src for workers + if (directive == nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE) { + mWorkerSrc = new nsCSPDirective(directive); + return mWorkerSrc; + } + + // if we have a script-src, cache it as a fallback for worker-src + // in case child-src is not present. It is also used as a fallback for + // script-src-elem and script-src-attr. + if (directive == nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) { + mScriptSrc = new nsCSPScriptSrcDirective(directive); + return mScriptSrc; + } + + // If we have a style-src, cache it as a fallback for style-src-elem and + // style-src-attr. + if (directive == nsIContentSecurityPolicy::STYLE_SRC_DIRECTIVE) { + mStyleSrc = new nsCSPStyleSrcDirective(directive); + return mStyleSrc; + } + + return new nsCSPDirective(directive); +} + +// 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. + if (mCurDir.Length() == 0) { + AutoTArray<nsString, 1> params = {u"directive missing"_ns}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "failedToParseUnrecognizedSource", params); + return; + } + + if (CSP_IsEmptyDirective(mCurValue, mCurToken)) { + 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) { + AutoTArray<nsString, 1> params = {u"block-all-mixed-content"_ns}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "ignoreSrcForDirective", 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) { + AutoTArray<nsString, 1> params = {u"upgrade-insecure-requests"_ns}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "ignoreSrcForDirective", params); + } + // add the directive and return + mPolicy->addUpgradeInsecDir( + static_cast<nsUpgradeInsecureDirective*>(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; + mHasAnyUnsafeEval = false; + mStrictDynamic = false; + mUnsafeInlineKeywordSrc = nullptr; + + mParsingFrameAncestorsDir = CSP_IsDirective( + mCurDir[0], nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE); + + // 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.IsEmpty() || (srcs.Length() == 1 && srcs[0]->isReportSample())) { + nsCSPKeywordSrc* keyword = new nsCSPKeywordSrc(CSP_NONE); + srcs.InsertElementAt(0, keyword); + } + + // If policy contains 'strict-dynamic' invalidate all srcs within script-src. + if (mStrictDynamic) { + MOZ_ASSERT( + cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) || + cspDir->equals( + nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) || + cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE), + "strict-dynamic only allowed within script-src(-elem|attr)"); + 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: 'strict-dynamic', 'unsafe-inline', + // 'unsafe-hashes', nonces, and hashes, because those still apply even + // with 'strict-dynamic'. + // TODO the comment seems wrong 'unsafe-eval' vs 'unsafe-inline'. + if (!srcStr.EqualsASCII(CSP_EnumToUTF8Keyword(CSP_STRICT_DYNAMIC)) && + !srcStr.EqualsASCII(CSP_EnumToUTF8Keyword(CSP_UNSAFE_EVAL)) && + !srcStr.EqualsASCII(CSP_EnumToUTF8Keyword(CSP_UNSAFE_HASHES)) && + !StringBeginsWith( + srcStr, nsDependentString(CSP_EnumToUTF16Keyword(CSP_NONCE))) && + !StringBeginsWith(srcStr, u"'sha"_ns)) { + AutoTArray<nsString, 2> params = {srcStr, mCurDir[0]}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "ignoringScriptSrcForStrictDynamic", params); + } + } + // Log a warning that all scripts might be blocked because the policy + // contains 'strict-dynamic' but no valid nonce or hash. + if (!mHasHashOrNonce) { + AutoTArray<nsString, 1> params = {mCurDir[0]}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "strictDynamicButNoHashOrNonce", params); + } + } + + // From https://w3c.github.io/webappsec-csp/#allow-all-inline + // follows that when either a hash or nonce is specified, 'unsafe-inline' + // should not apply. + if (mHasHashOrNonce && mUnsafeInlineKeywordSrc && + (cspDir->isDefaultDirective() || + cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) || + cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) || + cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE) || + cspDir->equals(nsIContentSecurityPolicy::STYLE_SRC_DIRECTIVE) || + cspDir->equals(nsIContentSecurityPolicy::STYLE_SRC_ELEM_DIRECTIVE) || + cspDir->equals(nsIContentSecurityPolicy::STYLE_SRC_ATTR_DIRECTIVE))) { + mUnsafeInlineKeywordSrc->invalidate(); + + // Log to the console that unsafe-inline will be ignored. + AutoTArray<nsString, 2> params = {u"'unsafe-inline'"_ns, mCurDir[0]}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "ignoringSrcWithinNonceOrHashDirective", params); + } + + if (mHasAnyUnsafeEval && + (cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) || + cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE))) { + // Log to the console that (wasm-)unsafe-eval will be ignored. + AutoTArray<nsString, 1> params = {mCurDir[0]}; + logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringUnsafeEval", + 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++) { + // https://w3c.github.io/webappsec-csp/#parse-serialized-policy + // Step 2.2. ..., or if token is not an ASCII string, continue. + // + // Note: In the spec the token isn't split by whitespace yet. + bool isAscii = true; + for (const auto& token : mTokens[i]) { + if (!IsAscii(token)) { + AutoTArray<nsString, 1> params = {mTokens[i][0], token}; + logWarningErrorToConsole(nsIScriptError::warningFlag, + "ignoringNonAsciiToken", params); + isAscii = false; + break; + } + } + if (!isAscii) { + continue; + } + + // 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].Clone(); + directive(); + } + + if (mChildSrc) { + if (!mFrameSrc) { + // if frame-src is specified explicitly for that policy than child-src + // should not restrict frames; if not, than child-src needs to restrict + // frames. + mChildSrc->setRestrictFrames(); + } + if (!mWorkerSrc) { + // if worker-src is specified explicitly for that policy than child-src + // should not restrict workers; if not, than child-src needs to restrict + // workers. + mChildSrc->setRestrictWorkers(); + } + } + + // if script-src is specified, but not worker-src and also no child-src, then + // script-src has to govern workers. + if (mScriptSrc && !mWorkerSrc && !mChildSrc) { + mScriptSrc->setRestrictWorkers(); + } + + // If script-src is specified and script-src-elem is not specified, then + // script-src has to govern script requests and script blocks. + if (mScriptSrc && !mPolicy->hasDirective( + nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE)) { + mScriptSrc->setRestrictScriptElem(); + } + + // If script-src is specified and script-src-attr is not specified, then + // script-src has to govern script attr (event handlers). + if (mScriptSrc && !mPolicy->hasDirective( + nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE)) { + mScriptSrc->setRestrictScriptAttr(); + } + + // If style-src is specified and style-src-elem is not specified, then + // style-src serves as a fallback. + if (mStyleSrc && !mPolicy->hasDirective( + nsIContentSecurityPolicy::STYLE_SRC_ELEM_DIRECTIVE)) { + mStyleSrc->setRestrictStyleElem(); + } + + // If style-src is specified and style-attr-elem is not specified, then + // style-src serves as a fallback. + if (mStyleSrc && !mPolicy->hasDirective( + nsIContentSecurityPolicy::STYLE_SRC_ATTR_DIRECTIVE)) { + mStyleSrc->setRestrictStyleAttr(); + } + + return mPolicy; +} + +nsCSPPolicy* nsCSPParser::parseContentSecurityPolicy( + const nsAString& aPolicyString, nsIURI* aSelfURI, bool aReportOnly, + nsCSPContext* aCSPContext, bool aDeliveredViaMetaTag, + bool aSuppressLogMessages) { + 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<CopyableTArray<nsString> > tokens; + PolicyTokenizer::tokenizePolicy(aPolicyString, tokens); + + nsCSPParser parser(tokens, aSelfURI, aCSPContext, aDeliveredViaMetaTag, + aSuppressLogMessages); + + // 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); + AutoTArray<nsString, 1> params; + CopyUTF8toUTF16(prePath, *params.AppendElement()); + parser.logWarningErrorToConsole(nsIScriptError::warningFlag, + "reportURInotInReportOnlyHeader", params); + } + } + + policy->setDeliveredViaMetaTagFlag(aDeliveredViaMetaTag); + + 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 0000000000..21679d86a0 --- /dev/null +++ b/dom/security/nsCSPParser.h @@ -0,0 +1,217 @@ +/* -*- 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 "nsCSPContext.h" +#include "nsIURI.h" +#include "PolicyTokenizer.h" + +bool isNumberToken(char16_t aSymbol); +bool isValidHexDig(char16_t aHexDig); + +// clang-format off +const char16_t COLON = ':'; +const char16_t SEMICOLON = ';'; +const char16_t SLASH = '/'; +const char16_t PLUS = '+'; +const char16_t DASH = '-'; +const char16_t DOT = '.'; +const char16_t UNDERLINE = '_'; +const char16_t TILDE = '~'; +const char16_t WILDCARD = '*'; +const char16_t SINGLEQUOTE = '\''; +const char16_t NUMBER_SIGN = '#'; +const char16_t QUESTIONMARK = '?'; +const char16_t PERCENT_SIGN = '%'; +const char16_t EXCLAMATION = '!'; +const char16_t DOLLAR = '$'; +const char16_t AMPERSAND = '&'; +const char16_t OPENBRACE = '('; +const char16_t CLOSINGBRACE = ')'; +const char16_t EQUALS = '='; +const char16_t ATSYMBOL = '@'; +// clang-format on + +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, + bool aSuppressLogMessages); + + private: + nsCSPParser(policyTokens& aTokens, nsIURI* aSelfURI, + nsCSPContext* aCSPContext, bool aDeliveredViaMetaTag, + bool aSuppressLogMessages); + + ~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 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* 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 nsTArray<nsString>& aParams); + + /** + * 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 mHasAnyUnsafeEval; // false, if no (wasm-)unsafe-eval keyword is used. + bool mStrictDynamic; // false, if 'strict-dynamic' is not defined + nsCSPKeywordSrc* mUnsafeInlineKeywordSrc; // null, otherwise invlidate() + + // cache variables for child-src, frame-src and worker-src handling; + // in CSP 3 child-src is deprecated. For backwards compatibility + // child-src needs to restrict: + // (*) frames, in case frame-src is not expicitly specified + // (*) workers, in case worker-src is not expicitly specified + // If neither worker-src, nor child-src is present, then script-src + // needs to govern workers. + nsCSPChildSrcDirective* mChildSrc; + nsCSPDirective* mFrameSrc; + nsCSPDirective* mWorkerSrc; + nsCSPScriptSrcDirective* mScriptSrc; + nsCSPStyleSrcDirective* mStyleSrc; + + // cache variable to let nsCSPHostSrc know that it's within + // the frame-ancestors directive. + bool mParsingFrameAncestorsDir; + + policyTokens mTokens; + nsIURI* mSelfURI; + nsCSPPolicy* mPolicy; + nsCSPContext* mCSPContext; // used for console logging + bool mDeliveredViaMetaTag; + bool mSuppressLogMessages; +}; + +#endif /* nsCSPParser_h___ */ diff --git a/dom/security/nsCSPService.cpp b/dom/security/nsCSPService.cpp new file mode 100644 index 0000000000..c5c65ad4d2 --- /dev/null +++ b/dom/security/nsCSPService.cpp @@ -0,0 +1,408 @@ +/* -*- 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 "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_security.h" +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsIURI.h" +#include "nsIContent.h" +#include "nsCSPService.h" +#include "nsIContentSecurityPolicy.h" +#include "nsError.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsAsyncRedirectVerifyHelper.h" +#include "nsContentUtils.h" +#include "nsContentPolicyUtils.h" +#include "nsNetUtil.h" +#include "nsIProtocolHandler.h" +#include "nsQueryObject.h" +#include "mozilla/net/DocumentLoadListener.h" +#include "mozilla/net/DocumentChannel.h" + +using namespace mozilla; + +static LazyLogModule gCspPRLog("CSP"); + +CSPService::CSPService() = default; + +CSPService::~CSPService() = default; + +NS_IMPL_ISUPPORTS(CSPService, nsIContentPolicy, nsIChannelEventSink) + +// Helper function to identify protocols and content types not subject to CSP. +bool subjectToCSP(nsIURI* aURI, nsContentPolicyType aContentType) { + ExtContentPolicyType contentType = + nsContentUtils::InternalContentPolicyTypeToExternal(aContentType); + + // These content types are not subject to CSP content policy checks: + // TYPE_CSP_REPORT -- csp can't block csp reports + // TYPE_DOCUMENT -- used for frame-ancestors + if (contentType == ExtContentPolicy::TYPE_CSP_REPORT || + contentType == ExtContentPolicy::TYPE_DOCUMENT) { + return false; + } + + // The three protocols: data:, blob: and filesystem: share the same + // protocol flag (URI_IS_LOCAL_RESOURCE) with other protocols, + // 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 + if (aURI->SchemeIs("data") || aURI->SchemeIs("blob") || + aURI->SchemeIs("filesystem")) { + return true; + } + + // Finally we have to allowlist "about:" which does not fall into + // the category underneath and also "javascript:" which is not + // subject to CSP content loading rules. + if (aURI->SchemeIs("about") || aURI->SchemeIs("javascript")) { + return false; + } + + // Please note that it should be possible for websites to + // allowlist their own protocol handlers with respect to CSP, + // hence we use protocol flags to accomplish that, but we also + // want resource:, chrome: and moz-icon to be subject to CSP + // (which also use URI_IS_LOCAL_RESOURCE). + // Exception to the rule are images, styles, and localization + // DTDs using a scheme of resource: or chrome: + bool isImgOrStyleOrDTD = contentType == ExtContentPolicy::TYPE_IMAGE || + contentType == ExtContentPolicy::TYPE_STYLESHEET || + contentType == ExtContentPolicy::TYPE_DTD; + if (aURI->SchemeIs("resource")) { + nsAutoCString uriSpec; + aURI->GetSpec(uriSpec); + // Exempt pdf.js from being subject to a page's CSP. + if (StringBeginsWith(uriSpec, "resource://pdf.js/"_ns)) { + return false; + } + if (!isImgOrStyleOrDTD) { + return true; + } + } + if (aURI->SchemeIs("chrome") && !isImgOrStyleOrDTD) { + return true; + } + if (aURI->SchemeIs("moz-icon")) { + return true; + } + bool match; + nsresult rv = NS_URIChainHasFlags( + aURI, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE, &match); + if (NS_SUCCEEDED(rv) && match) { + return false; + } + // all other protocols are subject To CSP. + return true; +} + +/* static */ nsresult CSPService::ConsultCSP(nsIURI* aContentLocation, + nsILoadInfo* aLoadInfo, + const nsACString& aMimeTypeGuess, + int16_t* aDecision) { + if (!aContentLocation) { + return NS_ERROR_FAILURE; + } + + nsContentPolicyType contentType = aLoadInfo->InternalContentPolicyType(); + bool parserCreatedScript = aLoadInfo->GetParserCreatedScript(); + + nsCOMPtr<nsICSPEventListener> cspEventListener; + nsresult rv = + aLoadInfo->GetCspEventListener(getter_AddRefs(cspEventListener)); + NS_ENSURE_SUCCESS(rv, rv); + + 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 allowlistet in subjectToCSP() + if (!subjectToCSP(aContentLocation, contentType)) { + return NS_OK; + } + + nsAutoString cspNonce; + rv = aLoadInfo->GetCspNonce(cspNonce); + NS_ENSURE_SUCCESS(rv, rv); + + // 1) Apply speculate CSP for preloads + bool isPreload = nsContentUtils::IsPreloadType(contentType); + + if (isPreload) { + nsCOMPtr<nsIContentSecurityPolicy> preloadCsp = aLoadInfo->GetPreloadCsp(); + if (preloadCsp) { + // obtain the enforcement decision + rv = preloadCsp->ShouldLoad( + contentType, cspEventListener, aContentLocation, + nullptr, // no redirect, aOriginal URL is null. + false, cspNonce, parserCreatedScript, 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)) { + NS_SetRequestBlockingReason( + aLoadInfo, nsILoadInfo::BLOCKING_REASON_CONTENT_POLICY_PRELOAD); + + return NS_OK; + } + } + } + + // 2) Apply actual CSP to all loads. Please note that in case + // the csp should be overruled (e.g. by an ExpandedPrincipal) + // then loadinfo->GetCsp() returns that CSP instead of the + // document's CSP. + nsCOMPtr<nsIContentSecurityPolicy> csp = aLoadInfo->GetCsp(); + + if (csp) { + // Generally aOriginalURI denotes the URI before a redirect and hence + // will always be a nullptr here. Only exception are frame navigations + // which we want to treat as a redirect for the purpose of CSP reporting + // and in particular the `blocked-uri` in the CSP report where we want + // to report the prePath information. + nsCOMPtr<nsIURI> originalURI = nullptr; + ExtContentPolicyType extType = + nsContentUtils::InternalContentPolicyTypeToExternal(contentType); + if (extType == ExtContentPolicy::TYPE_SUBDOCUMENT && + !aLoadInfo->GetOriginalFrameSrcLoad() && + mozilla::StaticPrefs:: + security_csp_truncate_blocked_uri_for_frame_navigations()) { + nsAutoCString prePathStr; + nsresult rv = aContentLocation->GetPrePath(prePathStr); + NS_ENSURE_SUCCESS(rv, rv); + rv = NS_NewURI(getter_AddRefs(originalURI), prePathStr); + NS_ENSURE_SUCCESS(rv, rv); + } + + // obtain the enforcement decision + rv = csp->ShouldLoad( + contentType, cspEventListener, aContentLocation, + originalURI, // no redirect, unless it's a frame navigation. + !isPreload && aLoadInfo->GetSendCSPViolationEvents(), cspNonce, + parserCreatedScript, aDecision); + + if (NS_CP_REJECTED(*aDecision)) { + NS_SetRequestBlockingReason( + aLoadInfo, nsILoadInfo::BLOCKING_REASON_CONTENT_POLICY_GENERAL); + } + + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +/* nsIContentPolicy implementation */ +NS_IMETHODIMP +CSPService::ShouldLoad(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo, + const nsACString& aMimeTypeGuess, int16_t* aDecision) { + return ConsultCSP(aContentLocation, aLoadInfo, aMimeTypeGuess, aDecision); +} + +NS_IMETHODIMP +CSPService::ShouldProcess(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo, + const nsACString& aMimeTypeGuess, + int16_t* aDecision) { + if (!aContentLocation) { + return NS_ERROR_FAILURE; + } + nsContentPolicyType contentType = aLoadInfo->InternalContentPolicyType(); + + 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 + // (contentType) to ShouldLoad(). + ExtContentPolicyType policyType = + nsContentUtils::InternalContentPolicyTypeToExternal(contentType); + + if (policyType != ExtContentPolicy::TYPE_OBJECT) { + *aDecision = nsIContentPolicy::ACCEPT; + return NS_OK; + } + + return ShouldLoad(aContentLocation, aLoadInfo, aMimeTypeGuess, aDecision); +} + +/* nsIChannelEventSink implementation */ +NS_IMETHODIMP +CSPService::AsyncOnChannelRedirect(nsIChannel* oldChannel, + nsIChannel* newChannel, uint32_t flags, + nsIAsyncVerifyRedirectCallback* callback) { + net::nsAsyncRedirectAutoCallback autoCallback(callback); + + if (XRE_IsE10sParentProcess()) { + nsCOMPtr<nsIParentChannel> parentChannel; + NS_QueryNotificationCallbacks(oldChannel, parentChannel); + RefPtr<net::DocumentLoadListener> docListener = + do_QueryObject(parentChannel); + // Since this is an IPC'd channel we do not have access to the request + // context. In turn, we do not have an event target for policy violations. + // Enforce the CSP check in the content process where we have that info. + // We allow redirect checks to run for document loads via + // DocumentLoadListener, since these are fully supported and we don't + // expose the redirects to the content process. We can't do this for all + // request types yet because we don't serialize nsICSPEventListener. + if (parentChannel && !docListener) { + return NS_OK; + } + } + + // Don't do these checks if we're switching from DocumentChannel + // to a real channel. In that case, we should already have done + // the checks in the parent process. AsyncOnChannelRedirect + // isn't called in the content process if we switch process, + // so checking here would just hide bugs in the process switch + // cases. + if (RefPtr<net::DocumentChannel> docChannel = do_QueryObject(oldChannel)) { + return NS_OK; + } + + nsCOMPtr<nsIURI> newUri; + nsresult rv = newChannel->GetURI(getter_AddRefs(newUri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILoadInfo> loadInfo = oldChannel->LoadInfo(); + + /* 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)); + if (NS_FAILED(rv)) { + autoCallback.DontCallback(); + oldChannel->Cancel(NS_ERROR_DOM_BAD_URI); + return rv; + } + + Maybe<nsresult> cancelCode; + rv = ConsultCSPForRedirect(originalUri, newUri, loadInfo, cancelCode); + if (cancelCode) { + oldChannel->Cancel(*cancelCode); + } + if (NS_FAILED(rv)) { + autoCallback.DontCallback(); + } + + return rv; +} + +nsresult CSPService::ConsultCSPForRedirect(nsIURI* aOriginalURI, + nsIURI* aNewURI, + nsILoadInfo* aLoadInfo, + Maybe<nsresult>& aCancelCode) { + // Check CSP navigate-to + // We need to enforce the CSP of the document that initiated the load, + // which is the CSP to inherit. + nsCOMPtr<nsIContentSecurityPolicy> cspToInherit = + aLoadInfo->GetCspToInherit(); + if (cspToInherit) { + bool allowsNavigateTo = false; + nsresult rv = cspToInherit->GetAllowsNavigateTo( + aNewURI, aLoadInfo->GetIsFormSubmission(), true, /* aWasRedirected */ + false, /* aEnforceAllowlist */ + &allowsNavigateTo); + NS_ENSURE_SUCCESS(rv, rv); + + if (!allowsNavigateTo) { + aCancelCode = Some(NS_ERROR_CSP_NAVIGATE_TO_VIOLATION); + 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 allowlistet in subjectToCSP() + nsContentPolicyType policyType = aLoadInfo->InternalContentPolicyType(); + if (!subjectToCSP(aNewURI, policyType)) { + return NS_OK; + } + + nsCOMPtr<nsICSPEventListener> cspEventListener; + nsresult rv = + aLoadInfo->GetCspEventListener(getter_AddRefs(cspEventListener)); + MOZ_ALWAYS_SUCCEEDS(rv); + + nsAutoString cspNonce; + rv = aLoadInfo->GetCspNonce(cspNonce); + MOZ_ALWAYS_SUCCEEDS(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 pass true to + * the aSendViolationReports parameter. See Bug 1219453. + */ + + int16_t decision = nsIContentPolicy::ACCEPT; + bool parserCreatedScript = aLoadInfo->GetParserCreatedScript(); + + // 1) Apply speculative CSP for preloads + if (isPreload) { + nsCOMPtr<nsIContentSecurityPolicy> preloadCsp = aLoadInfo->GetPreloadCsp(); + if (preloadCsp) { + // Pass originalURI to indicate the redirect + preloadCsp->ShouldLoad( + policyType, // load type per nsIContentPolicy (uint32_t) + cspEventListener, + aNewURI, // nsIURI + aOriginalURI, // Original nsIURI + true, // aSendViolationReports + cspNonce, // nonce + parserCreatedScript, &decision); + + // if the preload policy already denied the load, then there + // is no point in checking the real policy + if (NS_CP_REJECTED(decision)) { + aCancelCode = Some(NS_ERROR_DOM_BAD_URI); + return NS_BINDING_FAILED; + } + } + } + + // 2) Apply actual CSP to all loads + nsCOMPtr<nsIContentSecurityPolicy> csp = aLoadInfo->GetCsp(); + if (csp) { + // Pass originalURI to indicate the redirect + csp->ShouldLoad(policyType, // load type per nsIContentPolicy (uint32_t) + cspEventListener, + aNewURI, // nsIURI + aOriginalURI, // Original nsIURI + true, // aSendViolationReports + cspNonce, // nonce + parserCreatedScript, &decision); + if (NS_CP_REJECTED(decision)) { + aCancelCode = Some(NS_ERROR_DOM_BAD_URI); + return NS_BINDING_FAILED; + } + } + + return NS_OK; +} diff --git a/dom/security/nsCSPService.h b/dom/security/nsCSPService.h new file mode 100644 index 0000000000..fc0db250fe --- /dev/null +++ b/dom/security/nsCSPService.h @@ -0,0 +1,47 @@ +/* -*- 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" + +#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(); + + // helper function to avoid creating a new instance of the + // cspservice everytime we call content policies. + static nsresult ConsultCSP(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo, + const nsACString& aMimeTypeGuess, + int16_t* aDecision); + + // Static helper to check CSP when doing a channel redirect. + // Returns the results to returns from + // AsyncOnChannelRedirect/nsIAsyncVerifyRedirectCallback. Optionally returns + // an nsresult to Cancel the old channel with. + static nsresult ConsultCSPForRedirect(nsIURI* aOriginalURI, nsIURI* aNewURI, + nsILoadInfo* aLoadInfo, + mozilla::Maybe<nsresult>& aCancelCode); + + protected: + virtual ~CSPService(); +}; +#endif /* nsCSPService_h___ */ diff --git a/dom/security/nsCSPUtils.cpp b/dom/security/nsCSPUtils.cpp new file mode 100644 index 0000000000..c430c6ee3b --- /dev/null +++ b/dom/security/nsCSPUtils.cpp @@ -0,0 +1,1615 @@ +/* -*- 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 "nsCSPParser.h" +#include "nsComponentManagerUtils.h" +#include "nsIConsoleService.h" +#include "nsIChannel.h" +#include "nsICryptoHash.h" +#include "nsIScriptError.h" +#include "nsIStringBundle.h" +#include "nsIURL.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" +#include "nsSandboxFlags.h" +#include "nsServiceManagerUtils.h" + +#include "mozilla/Components.h" +#include "mozilla/dom/CSPDictionariesBinding.h" +#include "mozilla/dom/Document.h" +#include "mozilla/StaticPrefs_security.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; + } +} + +// The Content Security Policy should be inherited for +// local schemes like: "about", "blob", "data", or "filesystem". +// see: https://w3c.github.io/webappsec-csp/#initialize-document-csp +bool CSP_ShouldResponseInheritCSP(nsIChannel* aChannel) { + if (!aChannel) { + return false; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, false); + + bool isAbout = uri->SchemeIs("about"); + if (isAbout) { + nsAutoCString aboutSpec; + rv = uri->GetSpec(aboutSpec); + NS_ENSURE_SUCCESS(rv, false); + // also allow about:blank#foo + if (StringBeginsWith(aboutSpec, "about:blank"_ns) || + StringBeginsWith(aboutSpec, "about:srcdoc"_ns)) { + return true; + } + } + + return uri->SchemeIs("blob") || uri->SchemeIs("data") || + uri->SchemeIs("filesystem") || uri->SchemeIs("javascript"); +} + +void CSP_ApplyMetaCSPToDoc(mozilla::dom::Document& aDoc, + const nsAString& aPolicyStr) { + if (aDoc.IsLoadedAsData()) { + return; + } + + nsAutoString policyStr( + nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespace>( + aPolicyStr)); + + if (policyStr.IsEmpty()) { + return; + } + + nsCOMPtr<nsIContentSecurityPolicy> csp = aDoc.GetCsp(); + if (!csp) { + MOZ_ASSERT(false, "how come there is no CSP"); + return; + } + + // Multiple CSPs (delivered through either header of meta tag) need to + // be joined together, see: + // https://w3c.github.io/webappsec/specs/content-security-policy/#delivery-html-meta-element + nsresult rv = + csp->AppendPolicy(policyStr, + false, // csp via meta tag can not be report only + true); // delivered through the meta tag + NS_ENSURE_SUCCESS_VOID(rv); + if (nsPIDOMWindowInner* inner = aDoc.GetInnerWindow()) { + inner->SetCsp(csp); + } + aDoc.ApplySettingsFromCSP(false); +} + +void CSP_GetLocalizedStr(const char* aName, const nsTArray<nsString>& aParams, + nsAString& outResult) { + nsCOMPtr<nsIStringBundle> keyStringBundle; + nsCOMPtr<nsIStringBundleService> stringBundleService = + mozilla::components::StringBundle::Service(); + + 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, outResult); +} + +void CSP_LogStrMessage(const nsAString& aMsg) { + nsCOMPtr<nsIConsoleService> console( + do_GetService("@mozilla.org/consoleservice;1")); + + if (!console) { + return; + } + nsString msg(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 nsACString& aCategory, uint64_t aInnerWindowID, + bool aFromPrivateWindow) { + 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.AppendLiteral(u"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() && aLineNumber == 0) { + cspMsg.AppendLiteral(u"\nSource: "); + cspMsg.Append(aSourceLine); + } + + // Since we are leveraging csp errors as the category names which + // we pass to devtools, we should prepend them with "CSP_" to + // allow easy distincution in devtools code. e.g. + // upgradeInsecureRequest -> CSP_upgradeInsecureRequest + nsCString category("CSP_"); + category.Append(aCategory); + + nsresult rv; + if (aInnerWindowID > 0) { + rv = error->InitWithWindowID(cspMsg, aSourceName, aSourceLine, aLineNumber, + aColumnNumber, aFlags, category, + aInnerWindowID); + } else { + rv = error->Init(cspMsg, aSourceName, aSourceLine, aLineNumber, + aColumnNumber, aFlags, category, aFromPrivateWindow, + true /* from chrome context */); + } + if (NS_FAILED(rv)) { + return; + } + console->LogMessage(error); +} + +/** + * Combines CSP_LogMessage and CSP_GetLocalizedStr into one call. + */ +void CSP_LogLocalizedStr(const char* aName, const nsTArray<nsString>& aParams, + const nsAString& aSourceName, + const nsAString& aSourceLine, uint32_t aLineNumber, + uint32_t aColumnNumber, uint32_t aFlags, + const nsACString& aCategory, uint64_t aInnerWindowID, + bool aFromPrivateWindow) { + nsAutoString logMsg; + CSP_GetLocalizedStr(aName, aParams, logMsg); + CSP_LogMessage(logMsg, aSourceName, aSourceLine, aLineNumber, aColumnNumber, + aFlags, aCategory, aInnerWindowID, aFromPrivateWindow); +} + +/* ===== Helpers ============================ */ +// This implements +// https://w3c.github.io/webappsec-csp/#effective-directive-for-a-request. +// However the spec doesn't currently cover all request destinations, which +// we roughly represent using nsContentPolicyType. +CSPDirective CSP_ContentTypeToDirective(nsContentPolicyType aType) { + switch (aType) { + case nsIContentPolicy::TYPE_IMAGE: + case nsIContentPolicy::TYPE_IMAGESET: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON: + 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: + case nsIContentPolicy::TYPE_INTERNAL_MODULE: + case nsIContentPolicy::TYPE_INTERNAL_MODULE_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS: + case nsIContentPolicy::TYPE_INTERNAL_AUDIOWORKLET: + case nsIContentPolicy::TYPE_INTERNAL_PAINTWORKLET: + case nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_FRAME_MESSAGEMANAGER_SCRIPT: + // (https://github.com/w3c/webappsec-csp/issues/554) + // Some of these types are not explicitly defined in the spec. + // + // Chrome seems to use script-src-elem for worklet! + return nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE; + + case nsIContentPolicy::TYPE_STYLESHEET: + case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET: + case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET_PRELOAD: + return nsIContentSecurityPolicy::STYLE_SRC_ELEM_DIRECTIVE; + + case nsIContentPolicy::TYPE_FONT: + case nsIContentPolicy::TYPE_INTERNAL_FONT_PRELOAD: + return nsIContentSecurityPolicy::FONT_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_MEDIA: + case nsIContentPolicy::TYPE_INTERNAL_AUDIO: + case nsIContentPolicy::TYPE_INTERNAL_VIDEO: + case nsIContentPolicy::TYPE_INTERNAL_TRACK: + 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::WORKER_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_SUBDOCUMENT: + case nsIContentPolicy::TYPE_INTERNAL_FRAME: + case nsIContentPolicy::TYPE_INTERNAL_IFRAME: + 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: + case nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST: + case nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE: + case nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD: + case nsIContentPolicy::TYPE_WEB_IDENTITY: + return nsIContentSecurityPolicy::CONNECT_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_OBJECT: + case nsIContentPolicy::TYPE_OBJECT_SUBREQUEST: + case nsIContentPolicy::TYPE_INTERNAL_EMBED: + case nsIContentPolicy::TYPE_INTERNAL_OBJECT: + return nsIContentSecurityPolicy::OBJECT_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_DTD: + case nsIContentPolicy::TYPE_OTHER: + case nsIContentPolicy::TYPE_SPECULATIVE: + case nsIContentPolicy::TYPE_INTERNAL_DTD: + case nsIContentPolicy::TYPE_INTERNAL_FORCE_ALLOWED_DTD: + return nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE; + + // CSP does not apply to webrtc connections + case nsIContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA: + // 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; + + case nsIContentPolicy::TYPE_SAVEAS_DOWNLOAD: + case nsIContentPolicy::TYPE_UA_FONT: + return nsIContentSecurityPolicy::NO_DIRECTIVE; + + // Fall through to error for all other directives + // Note that we should never end up here for navigate-to + case nsIContentPolicy::TYPE_INVALID: + MOZ_ASSERT(false, "Can not map nsContentPolicyType to CSPDirective"); + // Do not add default: so that compilers can catch the missing case. + } + return nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE; +} + +nsCSPHostSrc* CSP_CreateHostSrcFromSelfURI(nsIURI* aSelfURI) { + // Create the host first + nsCString host; + aSelfURI->GetAsciiHost(host); + nsCSPHostSrc* hostsrc = new nsCSPHostSrc(NS_ConvertUTF8toUTF16(host)); + hostsrc->setGeneratedFromSelfKeyword(); + + // Add the scheme. + nsCString scheme; + aSelfURI->GetScheme(scheme); + hostsrc->setScheme(NS_ConvertUTF8toUTF16(scheme)); + + // An empty host (e.g. for data:) indicates it's effectively a unique origin. + // Please note that we still need to set the scheme on hostsrc (see above), + // because it's used for reporting. + if (host.EqualsLiteral("")) { + hostsrc->setIsUniqueOrigin(); + // no need to query the port in that case. + return hostsrc; + } + + int32_t port; + aSelfURI->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_IsEmptyDirective(const nsAString& aValue, const nsAString& aDir) { + return (aDir.Length() == 0 && aValue.Length() == 0); +} +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_EnumToUTF8Keyword(aKey)); +} + +bool CSP_IsQuotelessKeyword(const nsAString& aKey) { + nsString lowerKey; + ToLowerCase(aKey, lowerKey); + + nsAutoString keyword; + for (uint32_t i = 0; i < CSP_LAST_KEYWORD_VALUE; i++) { + // skipping the leading ' and trimming the trailing ' + keyword.AssignASCII(gCSPUTF8Keywords[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'. + * @param aFromSelfURI + * Whether a scheme was generated from the keyword 'self' + * which then allows schemeless sources to match ws and wss. + */ + +bool permitsScheme(const nsAString& aEnforcementScheme, nsIURI* aUri, + bool aReportOnly, bool aUpgradeInsecure, bool aFromSelfURI) { + 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")) { + if (scheme.EqualsASCII("https")) { + return true; + } + if ((scheme.EqualsASCII("ws") || scheme.EqualsASCII("wss")) && + aFromSelfURI) { + return true; + } + } + if (aEnforcementScheme.EqualsASCII("https")) { + if (scheme.EqualsLiteral("wss") && aFromSelfURI) { + return true; + } + } + if (aEnforcementScheme.EqualsASCII("ws") && scheme.EqualsASCII("wss")) { + 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; + for (const nsAString& policy : + nsCharSeparatedTokenizer(aHeaderValue, ',').ToRange()) { + 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() = default; + +// ::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_EnumToUTF8Keyword(aKeyword), + NS_ConvertUTF16toUTF8(aHashOrNonce).get())); + return false; +} + +/* ====== nsCSPSchemeSrc ===================== */ + +nsCSPSchemeSrc::nsCSPSchemeSrc(const nsAString& aScheme) : mScheme(aScheme) { + ToLowerCase(mScheme); +} + +nsCSPSchemeSrc::~nsCSPSchemeSrc() = default; + +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, false); +} + +bool nsCSPSchemeSrc::visit(nsCSPSrcVisitor* aVisitor) const { + return aVisitor->visitSchemeSrc(*this); +} + +void nsCSPSchemeSrc::toString(nsAString& outStr) const { + outStr.Append(mScheme); + outStr.AppendLiteral(":"); +} + +/* ===== nsCSPHostSrc ======================== */ + +nsCSPHostSrc::nsCSPHostSrc(const nsAString& aHost) + : mHost(aHost), + mGeneratedFromSelfKeyword(false), + mIsUniqueOrigin(false), + mWithinFrameAncstorsDir(false) { + ToLowerCase(mHost); +} + +nsCSPHostSrc::~nsCSPHostSrc() = default; + +/* + * 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); + if (NS_FAILED(rv) && aEnforcementPort.IsEmpty()) { + // If we cannot get a Port (e.g. because of an Custom Protocol handler) + // We need to check if a default port is associated with the Scheme + if (aEnforcementScheme.IsEmpty()) { + return false; + } + int defaultPortforScheme = + NS_GetDefaultPort(NS_ConvertUTF16toUTF8(aEnforcementScheme).get()); + + // If there is no default port associated with the Scheme ( + // defaultPortforScheme == -1) or it is an externally handled protocol ( + // defaultPortforScheme == 0 ) and the csp does not enforce a port - we can + // allow not having a port + return (defaultPortforScheme == -1 || defaultPortforScheme == -0); + } + // 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 || mIsUniqueOrigin) { + 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, + mGeneratedFromSelfKeyword)) { + 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"); + + // Before we can check if the host matches, we have to + // extract the host part from aUri. + nsAutoCString uriHost; + nsresult rv = aUri->GetAsciiHost(uriHost); + NS_ENSURE_SUCCESS(rv, false); + + nsString decodedUriHost; + CSP_PercentDecodeStr(NS_ConvertUTF8toUTF16(uriHost), decodedUriHost); + + // 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 allowlisting any of + // these schemes would call nsCSPSchemeSrc::permits(). + if (aUri->SchemeIs("blob") || aUri->SchemeIs("data") || + aUri->SchemeIs("filesystem")) { + return false; + } + + // If no scheme is present there also wont be a port and folder to check + // which means we can return early + if (mScheme.IsEmpty()) { + return true; + } + } + // 4.5) host matching: Check if the allowed host starts with a wilcard. + else 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); + + if (mWithinFrameAncstorsDir) { + // no path matching for frame-ancestors to not leak any path information. + return true; + } + + 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 refers to a specific file, and we have to + // check if the loading resource matches the 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 (mGeneratedFromSelfKeyword) { + outStr.AppendLiteral("'self'"); + return; + } + + // 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.AppendLiteral("://"); + outStr.Append(mHost); + + // append port + if (!mPort.IsEmpty()) { + outStr.AppendLiteral(":"); + 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() = default; + +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_EnumToUTF8Keyword(aKeyword), + NS_ConvertUTF16toUTF8(aHashOrNonce).get(), + mInvalidated ? "true" : "false")); + + if (mInvalidated) { + // only 'self', 'report-sample' 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 || mKeyword == CSP_REPORT_SAMPLE, + "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 and also eval & wasm-eval should be blocked even if + // 'strict-dynamic' is present. Should be allowed only if 'unsafe-eval' is + // present. + return ((mKeyword == aKeyword) || + ((mKeyword == CSP_STRICT_DYNAMIC) && !aParserCreated && + aKeyword != CSP_UNSAFE_EVAL && aKeyword != CSP_WASM_UNSAFE_EVAL)); +} + +bool nsCSPKeywordSrc::visit(nsCSPSrcVisitor* aVisitor) const { + return aVisitor->visitKeywordSrc(*this); +} + +void nsCSPKeywordSrc::toString(nsAString& outStr) const { + outStr.Append(CSP_EnumToUTF16Keyword(mKeyword)); +} + +/* ===== nsCSPNonceSrc ==================== */ + +nsCSPNonceSrc::nsCSPNonceSrc(const nsAString& aNonce) : mNonce(aNonce) {} + +nsCSPNonceSrc::~nsCSPNonceSrc() = default; + +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())); + } + + if (aReportOnly && aWasRedirected && aNonce.IsEmpty()) { + /* Fix for Bug 1505412 + * If we land here, we're currently handling a script-preload which got + * redirected. Preloads do not have any info about the nonce assiociated. + * Because of Report-Only the preload passes the 1st CSP-check so the + * preload does not get retried with a nonce attached. + * Currently we're relying on the script-manager to + * provide a fake loadinfo to check the preloads against csp. + * So during HTTPChannel->OnRedirect we cant check csp for this case. + * But as the script-manager already checked the csp, + * a report would already have been send, + * if the nonce didnt match. + * So we can pass the check here for Report-Only Cases. + */ + MOZ_ASSERT(aParserCreated == false, + "Skipping nonce-check is only allowed for Preloads"); + return true; + } + + // 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_EnumToUTF8Keyword(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.Append(CSP_EnumToUTF16Keyword(CSP_NONCE)); + outStr.Append(mNonce); + outStr.AppendLiteral("'"); +} + +/* ===== 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); + // Normalize the base64url encoding to base64 encoding: + char16_t* cur = mHash.BeginWriting(); + char16_t* end = mHash.EndWriting(); + + for (; cur < end; ++cur) { + if (char16_t('-') == *cur) { + *cur = char16_t('+'); + } + if (char16_t('_') == *cur) { + *cur = char16_t('/'); + } + } +} + +nsCSPHashSrc::~nsCSPHashSrc() = default; + +bool nsCSPHashSrc::allows(enum CSPKeyword aKeyword, + const nsAString& aHashOrNonce, + bool aParserCreated) const { + CSPUTILSLOG(("nsCSPHashSrc::allows, aKeyWord: %s, a HashOrNonce: %s", + CSP_EnumToUTF8Keyword(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); + + nsCOMPtr<nsICryptoHash> hasher; + nsresult rv = NS_NewCryptoHash(NS_ConvertUTF16toUTF8(mAlgorithm), + getter_AddRefs(hasher)); + 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); + + return NS_ConvertUTF16toUTF8(mHash).Equals(hash); +} + +bool nsCSPHashSrc::visit(nsCSPSrcVisitor* aVisitor) const { + return aVisitor->visitHashSrc(*this); +} + +void nsCSPHashSrc::toString(nsAString& outStr) const { + outStr.AppendLiteral("'"); + outStr.Append(mAlgorithm); + outStr.AppendLiteral("-"); + outStr.Append(mHash); + outStr.AppendLiteral("'"); +} + +/* ===== nsCSPReportURI ===================== */ + +nsCSPReportURI::nsCSPReportURI(nsIURI* aURI) : mReportURI(aURI) {} + +nsCSPReportURI::~nsCSPReportURI() = default; + +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() = default; + +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_EnumToUTF8Keyword(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.AppendLiteral(" "); + + // Append srcs + StringJoinAppend(outStr, u" "_ns, mSrcs, + [](nsAString& dest, nsCSPBaseSrc* cspBaseSrc) { + cspBaseSrc->toString(dest); + }); +} + +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); + if (!srcs.AppendElement(src, mozilla::fallible)) { + // XXX(Bug 1632090) Instead of extending the array 1-by-1 (which might + // involve multiple reallocations) and potentially crashing here, + // SetCapacity could be called outside the loop once. + mozalloc_handle_oom(0); + } + } + + switch (mDirective) { + case nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE: + outCSP.mDefault_src.Construct(); + outCSP.mDefault_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE: + outCSP.mScript_src.Construct(); + outCSP.mScript_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::OBJECT_SRC_DIRECTIVE: + outCSP.mObject_src.Construct(); + outCSP.mObject_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::STYLE_SRC_DIRECTIVE: + outCSP.mStyle_src.Construct(); + outCSP.mStyle_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::IMG_SRC_DIRECTIVE: + outCSP.mImg_src.Construct(); + outCSP.mImg_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::MEDIA_SRC_DIRECTIVE: + outCSP.mMedia_src.Construct(); + outCSP.mMedia_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::FRAME_SRC_DIRECTIVE: + outCSP.mFrame_src.Construct(); + outCSP.mFrame_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::FONT_SRC_DIRECTIVE: + outCSP.mFont_src.Construct(); + outCSP.mFont_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::CONNECT_SRC_DIRECTIVE: + outCSP.mConnect_src.Construct(); + outCSP.mConnect_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE: + outCSP.mReport_uri.Construct(); + outCSP.mReport_uri.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE: + outCSP.mFrame_ancestors.Construct(); + outCSP.mFrame_ancestors.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::WEB_MANIFEST_SRC_DIRECTIVE: + outCSP.mManifest_src.Construct(); + outCSP.mManifest_src.Value() = std::move(srcs); + return; + // not supporting REFLECTED_XSS_DIRECTIVE + + case nsIContentSecurityPolicy::BASE_URI_DIRECTIVE: + outCSP.mBase_uri.Construct(); + outCSP.mBase_uri.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::FORM_ACTION_DIRECTIVE: + outCSP.mForm_action.Construct(); + outCSP.mForm_action.Value() = std::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() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::SANDBOX_DIRECTIVE: + outCSP.mSandbox.Construct(); + outCSP.mSandbox.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE: + outCSP.mWorker_src.Construct(); + outCSP.mWorker_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE: + outCSP.mScript_src_elem.Construct(); + outCSP.mScript_src_elem.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE: + outCSP.mScript_src_attr.Construct(); + outCSP.mScript_src_attr.Value() = std::move(srcs); + return; + + default: + NS_ASSERTION(false, "cannot find directive to convert CSP to JSON"); + } +} + +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); +} + +void nsCSPDirective::getDirName(nsAString& outStr) const { + outStr.AppendASCII(CSP_CSPDirectiveToString(mDirective)); +} + +bool nsCSPDirective::hasReportSampleKeyword() const { + for (nsCSPBaseSrc* src : mSrcs) { + if (src->isReportSample()) { + return true; + } + } + + return false; +} + +/* =============== nsCSPChildSrcDirective ============= */ + +nsCSPChildSrcDirective::nsCSPChildSrcDirective(CSPDirective aDirective) + : nsCSPDirective(aDirective), + mRestrictFrames(false), + mRestrictWorkers(false) {} + +nsCSPChildSrcDirective::~nsCSPChildSrcDirective() = default; + +bool nsCSPChildSrcDirective::equals(CSPDirective aDirective) const { + if (aDirective == nsIContentSecurityPolicy::FRAME_SRC_DIRECTIVE) { + return mRestrictFrames; + } + if (aDirective == nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE) { + return mRestrictWorkers; + } + return (mDirective == aDirective); +} + +/* =============== nsCSPScriptSrcDirective ============= */ + +nsCSPScriptSrcDirective::nsCSPScriptSrcDirective(CSPDirective aDirective) + : nsCSPDirective(aDirective) {} + +nsCSPScriptSrcDirective::~nsCSPScriptSrcDirective() = default; + +bool nsCSPScriptSrcDirective::equals(CSPDirective aDirective) const { + if (aDirective == nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE) { + return mRestrictWorkers; + } + if (aDirective == nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) { + return mRestrictScriptElem; + } + if (aDirective == nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE) { + return mRestrictScriptAttr; + } + return mDirective == aDirective; +} + +/* =============== nsCSPStyleSrcDirective ============= */ + +nsCSPStyleSrcDirective::nsCSPStyleSrcDirective(CSPDirective aDirective) + : nsCSPDirective(aDirective) {} + +nsCSPStyleSrcDirective::~nsCSPStyleSrcDirective() = default; + +bool nsCSPStyleSrcDirective::equals(CSPDirective aDirective) const { + if (aDirective == nsIContentSecurityPolicy::STYLE_SRC_ELEM_DIRECTIVE) { + return mRestrictStyleElem; + } + if (aDirective == nsIContentSecurityPolicy::STYLE_SRC_ATTR_DIRECTIVE) { + return mRestrictStyleAttr; + } + return mDirective == aDirective; +} + +/* =============== nsBlockAllMixedContentDirective ============= */ + +nsBlockAllMixedContentDirective::nsBlockAllMixedContentDirective( + CSPDirective aDirective) + : nsCSPDirective(aDirective) {} + +nsBlockAllMixedContentDirective::~nsBlockAllMixedContentDirective() = default; + +void nsBlockAllMixedContentDirective::toString(nsAString& outStr) const { + outStr.AppendASCII(CSP_CSPDirectiveToString( + nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT)); +} + +void nsBlockAllMixedContentDirective::getDirName(nsAString& outStr) const { + outStr.AppendASCII(CSP_CSPDirectiveToString( + nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT)); +} + +/* =============== nsUpgradeInsecureDirective ============= */ + +nsUpgradeInsecureDirective::nsUpgradeInsecureDirective(CSPDirective aDirective) + : nsCSPDirective(aDirective) {} + +nsUpgradeInsecureDirective::~nsUpgradeInsecureDirective() = default; + +void nsUpgradeInsecureDirective::toString(nsAString& outStr) const { + outStr.AppendASCII(CSP_CSPDirectiveToString( + nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE)); +} + +void nsUpgradeInsecureDirective::getDirName(nsAString& outStr) const { + outStr.AppendASCII(CSP_CSPDirectiveToString( + nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE)); +} + +/* ===== nsCSPPolicy ========================= */ + +nsCSPPolicy::nsCSPPolicy() + : mUpgradeInsecDir(nullptr), + mReportOnly(false), + mDeliveredViaMetaTag(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, + 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]->getDirName(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->getDirName(outViolatedDirective); + return false; + } + return true; + } + + // Nothing restricts this, so we're allowing the load + // See bug 764937 + return true; +} + +bool nsCSPPolicy::allows(CSPDirective aDirective, enum CSPKeyword aKeyword, + const nsAString& aHashOrNonce, + bool aParserCreated) const { + CSPUTILSLOG(("nsCSPPolicy::allows, aKeyWord: %s, a HashOrNonce: %s", + CSP_EnumToUTF8Keyword(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]->isDefaultDirective()) { + defaultDir = mDirectives[i]; + continue; + } + if (mDirectives[i]->equals(aDirective)) { + if (mDirectives[i]->allows(aKeyword, aHashOrNonce, aParserCreated)) { + 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; +} + +void nsCSPPolicy::toString(nsAString& outStr) const { + StringJoinAppend(outStr, u"; "_ns, mDirectives, + [](nsAString& dest, nsCSPDirective* cspDirective) { + cspDirective->toString(dest); + }); +} + +void nsCSPPolicy::toDomCSPStruct(mozilla::dom::CSP& outCSP) const { + outCSP.mReport_only = mReportOnly; + + for (uint32_t i = 0; i < mDirectives.Length(); ++i) { + 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; +} + +bool nsCSPPolicy::allowsNavigateTo(nsIURI* aURI, bool aWasRedirected, + bool aEnforceAllowlist) const { + bool allowsNavigateTo = true; + + for (unsigned long i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->equals( + nsIContentSecurityPolicy::NAVIGATE_TO_DIRECTIVE)) { + // Early return if we can skip the allowlist AND 'unsafe-allow-redirects' + // is present. + if (!aEnforceAllowlist && + mDirectives[i]->allows(CSP_UNSAFE_ALLOW_REDIRECTS, u""_ns, false)) { + return true; + } + // Otherwise, check against the allowlist. + if (!mDirectives[i]->permits(aURI, u""_ns, aWasRedirected, false, false, + false)) { + allowsNavigateTo = false; + } + } + } + + return allowsNavigateTo; +} + +/* + * 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::getDirectiveStringAndReportSampleForContentType( + CSPDirective aDirective, nsAString& outDirective, + bool* aReportSample) const { + MOZ_ASSERT(aReportSample); + *aReportSample = false; + + nsCSPDirective* defaultDir = nullptr; + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->isDefaultDirective()) { + defaultDir = mDirectives[i]; + continue; + } + if (mDirectives[i]->equals(aDirective)) { + mDirectives[i]->getDirName(outDirective); + *aReportSample = mDirectives[i]->hasReportSampleKeyword(); + return; + } + } + // if we haven't found a matching directive yet, + // the contentType must be restricted by the default directive + if (defaultDir) { + defaultDir->getDirName(outDirective); + *aReportSample = defaultDir->hasReportSampleKeyword(); + return; + } + NS_ASSERTION(false, "Can not query directive string for contentType!"); + outDirective.AppendLiteral("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; +} diff --git a/dom/security/nsCSPUtils.h b/dom/security/nsCSPUtils.h new file mode 100644 index 0000000000..dd3ea9a1c4 --- /dev/null +++ b/dom/security/nsCSPUtils.h @@ -0,0 +1,699 @@ +/* -*- 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 "nsIContentSecurityPolicy.h" +#include "nsIURI.h" +#include "nsLiteralString.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsUnicharUtils.h" +#include "mozilla/Logging.h" + +class nsIChannel; + +namespace mozilla::dom { +struct CSP; +class Document; +} // namespace mozilla::dom + +/* =============== Logging =================== */ + +void CSP_LogLocalizedStr(const char* aName, const nsTArray<nsString>& aParams, + const nsAString& aSourceName, + const nsAString& aSourceLine, uint32_t aLineNumber, + uint32_t aColumnNumber, uint32_t aFlags, + const nsACString& aCategory, uint64_t aInnerWindowID, + bool aFromPrivateWindow); + +void CSP_GetLocalizedStr(const char* aName, const nsTArray<nsString>& aParams, + nsAString& 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 nsACString& aCategory, uint64_t aInnerWindowID, + bool aFromPrivateWindow); + +/* =============== 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 WASM_EVAL_VIOLATION_OBSERVER_TOPIC \ + "violated base restriction: WebAssembly code will not be created from " \ + "dynamically" +#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" + +// 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 + "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 + "sandbox", // SANDBOX_DIRECTIVE + "worker-src", // WORKER_SRC_DIRECTIVE + "navigate-to", // NAVIGATE_TO_DIRECTIVE + "script-src-elem", // SCRIPT_SRC_ELEM_DIRECTIVE + "script-src-attr", // SCRIPT_SRC_ATTR_DIRECTIVE + "style-src-elem", // STYLE_SRC_ELEM_DIRECTIVE + "style-src-attr", // STYLE_SRC_ATTR_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); + } + } + return nsIContentSecurityPolicy::NO_DIRECTIVE; +} + +#define FOR_EACH_CSP_KEYWORD(MACRO) \ + MACRO(CSP_SELF, "'self'") \ + MACRO(CSP_UNSAFE_INLINE, "'unsafe-inline'") \ + MACRO(CSP_UNSAFE_EVAL, "'unsafe-eval'") \ + MACRO(CSP_UNSAFE_HASHES, "'unsafe-hashes'") \ + MACRO(CSP_NONE, "'none'") \ + MACRO(CSP_NONCE, "'nonce-") \ + MACRO(CSP_REPORT_SAMPLE, "'report-sample'") \ + MACRO(CSP_STRICT_DYNAMIC, "'strict-dynamic'") \ + MACRO(CSP_UNSAFE_ALLOW_REDIRECTS, "'unsafe-allow-redirects'") \ + MACRO(CSP_WASM_UNSAFE_EVAL, "'wasm-unsafe-eval'") + +enum CSPKeyword { +#define KEYWORD_ENUM(id_, string_) id_, + FOR_EACH_CSP_KEYWORD(KEYWORD_ENUM) +#undef KEYWORD_ENUM + + // 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 +}; + +// The keywords, in UTF-8 form. +static const char* gCSPUTF8Keywords[] = { +#define KEYWORD_UTF8_LITERAL(id_, string_) string_, + FOR_EACH_CSP_KEYWORD(KEYWORD_UTF8_LITERAL) +#undef KEYWORD_UTF8_LITERAL +}; + +// The keywords, in UTF-16 form. +static const char16_t* gCSPUTF16Keywords[] = { +#define KEYWORD_UTF16_LITERAL(id_, string_) u"" string_, + FOR_EACH_CSP_KEYWORD(KEYWORD_UTF16_LITERAL) +#undef KEYWORD_UTF16_LITERAL +}; + +#undef FOR_EACH_CSP_KEYWORD + +inline const char* CSP_EnumToUTF8Keyword(enum CSPKeyword aKey) { + // Make sure all elements in enum CSPKeyword got added to gCSPUTF8Keywords. + static_assert((sizeof(gCSPUTF8Keywords) / sizeof(gCSPUTF8Keywords[0]) == + CSP_LAST_KEYWORD_VALUE), + "CSP_LAST_KEYWORD_VALUE != length(gCSPUTF8Keywords)"); + + if (static_cast<uint32_t>(aKey) < + static_cast<uint32_t>(CSP_LAST_KEYWORD_VALUE)) { + return gCSPUTF8Keywords[static_cast<uint32_t>(aKey)]; + } + return "error: invalid keyword in CSP_EnumToUTF8Keyword"; +} + +inline const char16_t* CSP_EnumToUTF16Keyword(enum CSPKeyword aKey) { + // Make sure all elements in enum CSPKeyword got added to gCSPUTF16Keywords. + static_assert((sizeof(gCSPUTF16Keywords) / sizeof(gCSPUTF16Keywords[0]) == + CSP_LAST_KEYWORD_VALUE), + "CSP_LAST_KEYWORD_VALUE != length(gCSPUTF16Keywords)"); + + if (static_cast<uint32_t>(aKey) < + static_cast<uint32_t>(CSP_LAST_KEYWORD_VALUE)) { + return gCSPUTF16Keywords[static_cast<uint32_t>(aKey)]; + } + return u"error: invalid keyword in CSP_EnumToUTF16Keyword"; +} + +inline CSPKeyword CSP_UTF16KeywordToEnum(const nsAString& aKey) { + nsString lowerKey = PromiseFlatString(aKey); + ToLowerCase(lowerKey); + + for (uint32_t i = 0; i < CSP_LAST_KEYWORD_VALUE; i++) { + if (lowerKey.Equals(gCSPUTF16Keywords[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_CreateHostSrcFromSelfURI(nsIURI* aSelfURI); +bool CSP_IsEmptyDirective(const nsAString& aValue, 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); +bool CSP_ShouldResponseInheritCSP(nsIChannel* aChannel); + +void CSP_ApplyMetaCSPToDoc(mozilla::dom::Document& aDoc, + const nsAString& aPolicyStr); + +/* =============== 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; } + + virtual bool isReportSample() const { return false; } + + 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 override; + bool visit(nsCSPSrcVisitor* aVisitor) const override; + void toString(nsAString& outStr) const override; + + 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 override; + bool visit(nsCSPSrcVisitor* aVisitor) const override; + void toString(nsAString& outStr) const override; + + void setScheme(const nsAString& aScheme); + void setPort(const nsAString& aPort); + void appendPath(const nsAString& aPath); + + inline void setGeneratedFromSelfKeyword() const { + mGeneratedFromSelfKeyword = true; + } + + inline void setIsUniqueOrigin() const { mIsUniqueOrigin = true; } + + inline void setWithinFrameAncestorsDir(bool aValue) const { + mWithinFrameAncstorsDir = aValue; + } + + 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; + mutable bool mGeneratedFromSelfKeyword; + mutable bool mIsUniqueOrigin; + mutable bool mWithinFrameAncstorsDir; +}; + +/* =============== nsCSPKeywordSrc ============ */ + +class nsCSPKeywordSrc : public nsCSPBaseSrc { + public: + explicit nsCSPKeywordSrc(CSPKeyword aKeyword); + virtual ~nsCSPKeywordSrc(); + + bool allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const override; + bool permits(nsIURI* aUri, const nsAString& aNonce, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure, + bool aParserCreated) const override; + bool visit(nsCSPSrcVisitor* aVisitor) const override; + void toString(nsAString& outStr) const override; + + inline CSPKeyword getKeyword() const { return mKeyword; }; + + inline void invalidate() const override { + // keywords that need to invalidated + if (mKeyword == CSP_SELF || mKeyword == CSP_UNSAFE_INLINE || + mKeyword == CSP_REPORT_SAMPLE) { + mInvalidated = true; + } + } + + bool isReportSample() const override { return mKeyword == CSP_REPORT_SAMPLE; } + + 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 override; + bool allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const override; + bool visit(nsCSPSrcVisitor* aVisitor) const override; + void toString(nsAString& outStr) const override; + + inline void getNonce(nsAString& outStr) const { outStr.Assign(mNonce); }; + + inline void invalidate() const override { + // 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 override; + void toString(nsAString& outStr) const override; + bool visit(nsCSPSrcVisitor* aVisitor) const override; + + inline void getAlgorithm(nsAString& outStr) const { + outStr.Assign(mAlgorithm); + }; + + inline void getHash(nsAString& outStr) const { outStr.Assign(mHash); }; + + inline void invalidate() const override { + // 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 override; + void toString(nsAString& outStr) const override; + + private: + nsCOMPtr<nsIURI> mReportURI; +}; + +/* =============== nsCSPSandboxFlags ================== */ + +class nsCSPSandboxFlags : public nsCSPBaseSrc { + public: + explicit nsCSPSandboxFlags(const nsAString& aFlags); + virtual ~nsCSPSandboxFlags(); + + bool visit(nsCSPSrcVisitor* aVisitor) const override; + void toString(nsAString& outStr) const override; + + 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() = default; + virtual ~nsCSPSrcVisitor() = default; +}; + +/* =============== 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.Clone(); + } + + 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; + + virtual void getDirName(nsAString& outStr) const; + + bool hasReportSampleKeyword() const; + + protected: + CSPDirective mDirective; + nsTArray<nsCSPBaseSrc*> mSrcs; +}; + +/* =============== nsCSPChildSrcDirective ============= */ + +/* + * In CSP 3 child-src is deprecated. For backwards compatibility + * child-src needs to restrict: + * (*) frames, in case frame-src is not expicitly specified + * (*) workers, in case worker-src is not expicitly specified + */ +class nsCSPChildSrcDirective : public nsCSPDirective { + public: + explicit nsCSPChildSrcDirective(CSPDirective aDirective); + virtual ~nsCSPChildSrcDirective(); + + void setRestrictFrames() { mRestrictFrames = true; } + + void setRestrictWorkers() { mRestrictWorkers = true; } + + virtual bool equals(CSPDirective aDirective) const override; + + private: + bool mRestrictFrames; + bool mRestrictWorkers; +}; + +/* =============== nsCSPScriptSrcDirective ============= */ + +/* + * In CSP 3 worker-src restricts workers, for backwards compatibily + * script-src has to restrict workers as the ultimate fallback if + * neither worker-src nor child-src is present in a CSP. + */ +class nsCSPScriptSrcDirective : public nsCSPDirective { + public: + explicit nsCSPScriptSrcDirective(CSPDirective aDirective); + virtual ~nsCSPScriptSrcDirective(); + + void setRestrictWorkers() { mRestrictWorkers = true; } + void setRestrictScriptElem() { mRestrictScriptElem = true; } + void setRestrictScriptAttr() { mRestrictScriptAttr = true; } + + bool equals(CSPDirective aDirective) const override; + + private: + bool mRestrictWorkers = false; + bool mRestrictScriptElem = false; + bool mRestrictScriptAttr = false; +}; + +/* =============== nsCSPStyleSrcDirective ============= */ + +/* + * In CSP 3 style-src is use as a fallback for style-src-elem and + * style-src-attr. + */ +class nsCSPStyleSrcDirective : public nsCSPDirective { + public: + explicit nsCSPStyleSrcDirective(CSPDirective aDirective); + virtual ~nsCSPStyleSrcDirective(); + + void setRestrictStyleElem() { mRestrictStyleElem = true; } + void setRestrictStyleAttr() { mRestrictStyleAttr = true; } + + bool equals(CSPDirective aDirective) const override; + + private: + bool mRestrictStyleElem = false; + bool mRestrictStyleAttr = false; +}; + +/* =============== 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 override { + return false; + } + + bool permits(nsIURI* aUri) const { return false; } + + bool allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const override { + return false; + } + + void toString(nsAString& outStr) const override; + + void addSrcs(const nsTArray<nsCSPBaseSrc*>& aSrcs) override { + MOZ_ASSERT(false, "block-all-mixed-content does not hold any srcs"); + } + + void getDirName(nsAString& outStr) const override; +}; + +/* =============== nsUpgradeInsecureDirective === */ + +/* + * Upgrading insecure requests includes the following actors: + * (1) CSP: + * The CSP implementation allowlists 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 allowlists 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 override { + return false; + } + + bool permits(nsIURI* aUri) const { return false; } + + bool allows(enum CSPKeyword aKeyword, const nsAString& aHashOrNonce, + bool aParserCreated) const override { + return false; + } + + void toString(nsAString& outStr) const override; + + void addSrcs(const nsTArray<nsCSPBaseSrc*>& aSrcs) override { + MOZ_ASSERT(false, "upgrade-insecure-requests does not hold any srcs"); + } + + void getDirName(nsAString& outStr) const override; +}; + +/* =============== 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 allows(CSPDirective aDirective, enum CSPKeyword aKeyword, + const nsAString& aHashOrNonce, bool aParserCreated) 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 setDeliveredViaMetaTagFlag(bool aFlag) { + mDeliveredViaMetaTag = aFlag; + } + + inline bool getDeliveredViaMetaTagFlag() const { + return mDeliveredViaMetaTag; + } + + inline void setReportOnlyFlag(bool aFlag) { mReportOnly = aFlag; } + + inline bool getReportOnlyFlag() const { return mReportOnly; } + + void getReportURIs(nsTArray<nsString>& outReportURIs) const; + + void getDirectiveStringAndReportSampleForContentType( + CSPDirective aDirective, nsAString& outDirective, + bool* aReportSample) const; + + void getDirectiveAsString(CSPDirective aDir, nsAString& outDirective) const; + + uint32_t getSandboxFlags() const; + + inline uint32_t getNumDirectives() const { return mDirectives.Length(); } + + bool visitDirectiveSrcs(CSPDirective aDir, nsCSPSrcVisitor* aVisitor) const; + + bool allowsNavigateTo(nsIURI* aURI, bool aWasRedirected, + bool aEnforceAllowlist) const; + + private: + nsUpgradeInsecureDirective* mUpgradeInsecDir; + nsTArray<nsCSPDirective*> mDirectives; + bool mReportOnly; + bool mDeliveredViaMetaTag; +}; + +#endif /* nsCSPUtils_h___ */ diff --git a/dom/security/nsContentSecurityManager.cpp b/dom/security/nsContentSecurityManager.cpp new file mode 100644 index 0000000000..a3a38c9776 --- /dev/null +++ b/dom/security/nsContentSecurityManager.cpp @@ -0,0 +1,1849 @@ +/* -*- 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 "nsAboutProtocolUtils.h" +#include "nsArray.h" +#include "nsContentSecurityManager.h" +#include "nsContentSecurityUtils.h" +#include "nsContentPolicyUtils.h" +#include "nsEscape.h" +#include "nsDataHandler.h" +#include "nsIChannel.h" +#include "nsIContentPolicy.h" +#include "nsIHttpChannelInternal.h" +#include "nsINode.h" +#include "nsIStreamListener.h" +#include "nsILoadInfo.h" +#include "nsIMIMEService.h" +#include "nsIOService.h" +#include "nsContentUtils.h" +#include "nsCORSListenerProxy.h" +#include "nsIParentChannel.h" +#include "nsIRedirectHistoryEntry.h" +#include "nsIXULRuntime.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" +#include "nsSandboxFlags.h" +#include "nsIXPConnect.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/nsMixedContentBlocker.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Document.h" +#include "mozilla/extensions/WebExtensionPolicy.h" +#include "mozilla/Components.h" +#include "mozilla/ExtensionPolicyService.h" +#include "mozilla/Logging.h" +#include "mozilla/Maybe.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryComms.h" +#include "xpcpublic.h" +#include "nsMimeTypes.h" + +#include "jsapi.h" +#include "js/RegExp.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::Telemetry; + +NS_IMPL_ISUPPORTS(nsContentSecurityManager, nsIContentSecurityManager, + nsIChannelEventSink) + +mozilla::LazyLogModule sCSMLog("CSMLog"); + +// These first two are used for off-the-main-thread checks of +// general.config.filename +// (which can't be checked off-main-thread). +Atomic<bool, mozilla::Relaxed> sJSHacksChecked(false); +Atomic<bool, mozilla::Relaxed> sJSHacksPresent(false); +Atomic<bool, mozilla::Relaxed> sCSSHacksChecked(false); +Atomic<bool, mozilla::Relaxed> sCSSHacksPresent(false); +Atomic<bool, mozilla::Relaxed> sTelemetryEventEnabled(false); + +/* static */ +bool nsContentSecurityManager::AllowTopLevelNavigationToDataURI( + nsIChannel* aChannel) { + // Let's block all toplevel document navigations to a data: URI. + // In all cases where the toplevel document is navigated to a + // data: URI the triggeringPrincipal is a contentPrincipal, or + // a NullPrincipal. In other cases, e.g. typing a data: URL into + // the URL-Bar, the triggeringPrincipal is a SystemPrincipal; + // we don't want to block those loads. Only exception, loads coming + // from an external applicaton (e.g. Thunderbird) don't load + // using a contentPrincipal, but we want to block those loads. + if (!StaticPrefs::security_data_uri_block_toplevel_data_uri_navigations()) { + return true; + } + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + if (loadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) { + return true; + } + if (loadInfo->GetForceAllowDataURI()) { + // if the loadinfo explicitly allows the data URI navigation, let's allow it + // now + return true; + } + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, true); + bool isDataURI = uri->SchemeIs("data"); + if (!isDataURI) { + return true; + } + + nsAutoCString spec; + rv = uri->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, true); + nsAutoCString contentType; + bool base64; + rv = nsDataHandler::ParseURI(spec, contentType, nullptr, base64, nullptr); + NS_ENSURE_SUCCESS(rv, true); + + // Allow data: images as long as they are not SVGs + if (StringBeginsWith(contentType, "image/"_ns) && + !contentType.EqualsLiteral("image/svg+xml")) { + return true; + } + // Allow all data: PDFs. or JSON documents + if (contentType.EqualsLiteral(APPLICATION_JSON) || + contentType.EqualsLiteral(TEXT_JSON) || + contentType.EqualsLiteral(APPLICATION_PDF)) { + return true; + } + // Redirecting to a toplevel data: URI is not allowed, hence we make + // sure the RedirectChain is empty. + if (!loadInfo->GetLoadTriggeredFromExternal() && + loadInfo->TriggeringPrincipal()->IsSystemPrincipal() && + loadInfo->RedirectChain().IsEmpty()) { + return true; + } + + // We're going to block the request, construct the localized error message to + // report to the console. + nsAutoCString dataSpec; + uri->GetSpec(dataSpec); + if (dataSpec.Length() > 50) { + dataSpec.Truncate(50); + dataSpec.AppendLiteral("..."); + } + AutoTArray<nsString, 1> params; + CopyUTF8toUTF16(NS_UnescapeURL(dataSpec), *params.AppendElement()); + nsAutoString errorText; + rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eSECURITY_PROPERTIES, "BlockTopLevelDataURINavigation", + params, errorText); + NS_ENSURE_SUCCESS(rv, false); + + // Report the localized error message to the console for the loading + // BrowsingContext's current inner window. + RefPtr<BrowsingContext> target = loadInfo->GetBrowsingContext(); + nsContentUtils::ReportToConsoleByWindowID( + errorText, nsIScriptError::warningFlag, "DATA_URI_BLOCKED"_ns, + target ? target->GetCurrentInnerWindowId() : 0); + return false; +} + +/* static */ +bool nsContentSecurityManager::AllowInsecureRedirectToDataURI( + nsIChannel* aNewChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aNewChannel->LoadInfo(); + if (loadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_SCRIPT) { + return true; + } + nsCOMPtr<nsIURI> newURI; + nsresult rv = NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(newURI)); + if (NS_FAILED(rv) || !newURI) { + return true; + } + bool isDataURI = newURI->SchemeIs("data"); + if (!isDataURI) { + return true; + } + + // Web Extensions are exempt from that restriction and are allowed to redirect + // a channel to a data: URI. When a web extension redirects a channel, we set + // a flag on the loadInfo which allows us to identify such redirects here. + if (loadInfo->GetAllowInsecureRedirectToDataURI()) { + return true; + } + + nsAutoCString dataSpec; + newURI->GetSpec(dataSpec); + if (dataSpec.Length() > 50) { + dataSpec.Truncate(50); + dataSpec.AppendLiteral("..."); + } + nsCOMPtr<Document> doc; + nsINode* node = loadInfo->LoadingNode(); + if (node) { + doc = node->OwnerDoc(); + } + AutoTArray<nsString, 1> params; + CopyUTF8toUTF16(NS_UnescapeURL(dataSpec), *params.AppendElement()); + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + "DATA_URI_BLOCKED"_ns, doc, + nsContentUtils::eSECURITY_PROPERTIES, + "BlockSubresourceRedirectToData", params); + return false; +} + +/* static */ +nsresult nsContentSecurityManager::CheckFTPSubresourceLoad( + nsIChannel* aChannel) { + // We dissallow using FTP resources as a subresource everywhere. + // The only valid way to use FTP resources is loading it as + // a top level document. + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + ExtContentPolicyType type = loadInfo->GetExternalContentPolicyType(); + + // Allow top-level FTP documents and save-as download of FTP files on + // HTTP pages. + if (type == ExtContentPolicy::TYPE_DOCUMENT || + type == ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD) { + return NS_OK; + } + + // Allow the system principal to load everything. This is meant to + // temporarily fix downloads and pdf.js. + nsIPrincipal* triggeringPrincipal = loadInfo->TriggeringPrincipal(); + if (triggeringPrincipal->IsSystemPrincipal()) { + return NS_OK; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + if (!uri) { + return NS_OK; + } + + bool isFtpURI = uri->SchemeIs("ftp"); + if (!isFtpURI) { + return NS_OK; + } + + nsCOMPtr<Document> doc; + if (nsINode* node = loadInfo->LoadingNode()) { + doc = node->OwnerDoc(); + } + + nsAutoCString spec; + uri->GetSpec(spec); + AutoTArray<nsString, 1> params; + CopyUTF8toUTF16(NS_UnescapeURL(spec), *params.AppendElement()); + + nsContentUtils::ReportToConsole( + nsIScriptError::warningFlag, "FTP_URI_BLOCKED"_ns, doc, + nsContentUtils::eSECURITY_PROPERTIES, "BlockSubresourceFTP", params); + + return NS_ERROR_CONTENT_BLOCKED; +} + +static nsresult ValidateSecurityFlags(nsILoadInfo* aLoadInfo) { + nsSecurityFlags securityMode = aLoadInfo->GetSecurityMode(); + + // We should never perform a security check on a loadInfo that uses the flag + // SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, because that is only used for + // temporary loadInfos used for explicit nsIContentPolicy checks, but never be + // set as a security flag on an actual channel. + if (securityMode != + nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT && + securityMode != nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED && + securityMode != + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT && + securityMode != nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL && + securityMode != nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) { + 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 already_AddRefed<nsIPrincipal> GetExtensionSandboxPrincipal( + nsILoadInfo* aLoadInfo) { + // An extension is allowed to load resources from itself when its pages are + // loaded into a sandboxed frame. Extension resources in a sandbox have + // a null principal and no access to extension APIs. See "sandbox" in + // MDN extension docs for more information. + if (!aLoadInfo->TriggeringPrincipal()->GetIsNullPrincipal()) { + return nullptr; + } + RefPtr<Document> doc; + aLoadInfo->GetLoadingDocument(getter_AddRefs(doc)); + if (!doc || !(doc->GetSandboxFlags() & SANDBOXED_ORIGIN)) { + return nullptr; + } + + // node principal is also a null principal here, so we need to + // create a principal using documentURI, which is the moz-extension + // uri for the page if this is an extension sandboxed page. + nsCOMPtr<nsIPrincipal> docPrincipal = BasePrincipal::CreateContentPrincipal( + doc->GetDocumentURI(), doc->NodePrincipal()->OriginAttributesRef()); + + if (!BasePrincipal::Cast(docPrincipal)->AddonPolicy()) { + return nullptr; + } + return docPrincipal.forget(); +} + +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; + } + + auto appType = nsIDocShell::APP_TYPE_UNKNOWN; + nsINode* node = aLoadInfo->LoadingNode(); + if (!node) { + return false; + } + Document* doc = node->OwnerDoc(); + if (!doc) { + return false; + } + + nsCOMPtr<nsIDocShellTreeItem> docShellTreeItem = doc->GetDocShell(); + if (!docShellTreeItem) { + return false; + } + + nsCOMPtr<nsIDocShellTreeItem> root; + docShellTreeItem->GetInProcessRootTreeItem(getter_AddRefs(root)); + nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(root)); + if (docShell) { + appType = docShell->GetAppType(); + } + + return appType == nsIDocShell::APP_TYPE_EDITOR; +} + +static nsresult DoCheckLoadURIChecks(nsIURI* aURI, nsILoadInfo* aLoadInfo) { + // In practice, these DTDs are just used for localization, so applying the + // same principal check as Fluent. + if (aLoadInfo->InternalContentPolicyType() == + nsIContentPolicy::TYPE_INTERNAL_DTD) { + RefPtr<Document> doc; + aLoadInfo->GetLoadingDocument(getter_AddRefs(doc)); + bool allowed = false; + aLoadInfo->TriggeringPrincipal()->IsL10nAllowed( + doc ? doc->GetDocumentURI() : nullptr, &allowed); + + return allowed ? NS_OK : NS_ERROR_DOM_BAD_URI; + } + + // This is used in order to allow a privileged DOMParser to parse documents + // that need to access localization DTDs. We just allow through + // TYPE_INTERNAL_FORCE_ALLOWED_DTD no matter what the triggering principal is. + if (aLoadInfo->InternalContentPolicyType() == + nsIContentPolicy::TYPE_INTERNAL_FORCE_ALLOWED_DTD) { + return NS_OK; + } + + if (IsImageLoadInEditorAppType(aLoadInfo)) { + return NS_OK; + } + + nsCOMPtr<nsIPrincipal> triggeringPrincipal = aLoadInfo->TriggeringPrincipal(); + nsCOMPtr<nsIPrincipal> addonPrincipal = + GetExtensionSandboxPrincipal(aLoadInfo); + if (addonPrincipal) { + // call CheckLoadURIWithPrincipal() as below to continue other checks, but + // with the addon principal. + triggeringPrincipal = addonPrincipal; + } + + // 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( + triggeringPrincipal, aURI, aLoadInfo->CheckLoadURIFlags(), + aLoadInfo->GetInnerWindowID()); +} + +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) || + nsContentUtils::SchemeIs(aURI, "moz-safe-about"))) { + // UI resources are allowed. + return DoCheckLoadURIChecks(aURI, aLoadInfo); + } + + if (NS_HasBeenCrossOrigin(aChannel, true)) { + NS_SetRequestBlockingReason(aLoadInfo, + nsILoadInfo::BLOCKING_REASON_NOT_SAME_ORIGIN); + return 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. + if (aLoadInfo->TriggeringPrincipal()->IsSystemPrincipal()) { + return NS_OK; + } + + // We use the triggering principal here, rather than the loading principal + // to ensure that anonymous CORS content in the browser resources and in + // WebExtensions is allowed to load. + nsIPrincipal* principal = aLoadInfo->TriggeringPrincipal(); + RefPtr<nsCORSListenerProxy> corsListener = new nsCORSListenerProxy( + aInAndOutListener, principal, + 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) { + ExtContentPolicyType contentPolicyType = + aLoadInfo->GetExternalContentPolicyType(); + nsContentPolicyType internalContentPolicyType = + aLoadInfo->InternalContentPolicyType(); + nsCString mimeTypeGuess; + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + switch (contentPolicyType) { + case ExtContentPolicy::TYPE_OTHER: { + mimeTypeGuess.Truncate(); + break; + } + + case ExtContentPolicy::TYPE_SCRIPT: { + mimeTypeGuess = "application/javascript"_ns; + break; + } + + case ExtContentPolicy::TYPE_IMAGE: { + mimeTypeGuess.Truncate(); + break; + } + + case ExtContentPolicy::TYPE_STYLESHEET: { + mimeTypeGuess = "text/css"_ns; + break; + } + + case ExtContentPolicy::TYPE_OBJECT: { + mimeTypeGuess.Truncate(); + break; + } + + case ExtContentPolicy::TYPE_DOCUMENT: { + mimeTypeGuess.Truncate(); + break; + } + + case ExtContentPolicy::TYPE_SUBDOCUMENT: { + mimeTypeGuess = "text/html"_ns; + break; + } + + case ExtContentPolicy::TYPE_PING: { + mimeTypeGuess.Truncate(); + break; + } + + case ExtContentPolicy::TYPE_XMLHTTPREQUEST: { +#ifdef DEBUG + { + nsCOMPtr<nsINode> node = aLoadInfo->LoadingNode(); + MOZ_ASSERT(!node || node->NodeType() == nsINode::DOCUMENT_NODE, + "type_xml requires requestingContext of type Document"); + } +#endif + // 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.Truncate(); + } else { + MOZ_ASSERT(internalContentPolicyType == + nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE, + "can not set mime type guess for unexpected internal type"); + mimeTypeGuess = nsLiteralCString(TEXT_EVENT_STREAM); + } + break; + } + + case ExtContentPolicy::TYPE_OBJECT_SUBREQUEST: { + mimeTypeGuess.Truncate(); +#ifdef DEBUG + { + nsCOMPtr<nsINode> node = aLoadInfo->LoadingNode(); + MOZ_ASSERT( + !node || node->NodeType() == nsINode::ELEMENT_NODE, + "type_subrequest requires requestingContext of type Element"); + } +#endif + break; + } + + case ExtContentPolicy::TYPE_DTD: { + mimeTypeGuess.Truncate(); +#ifdef DEBUG + { + nsCOMPtr<nsINode> node = aLoadInfo->LoadingNode(); + MOZ_ASSERT(!node || node->NodeType() == nsINode::DOCUMENT_NODE, + "type_dtd requires requestingContext of type Document"); + } +#endif + break; + } + + case ExtContentPolicy::TYPE_FONT: + case ExtContentPolicy::TYPE_UA_FONT: { + mimeTypeGuess.Truncate(); + break; + } + + case ExtContentPolicy::TYPE_MEDIA: { + if (internalContentPolicyType == nsIContentPolicy::TYPE_INTERNAL_TRACK) { + mimeTypeGuess = "text/vtt"_ns; + } else { + mimeTypeGuess.Truncate(); + } +#ifdef DEBUG + { + nsCOMPtr<nsINode> node = aLoadInfo->LoadingNode(); + MOZ_ASSERT(!node || node->NodeType() == nsINode::ELEMENT_NODE, + "type_media requires requestingContext of type Element"); + } +#endif + break; + } + + case ExtContentPolicy::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) { + rv = httpChannelInternal->GetProxyURI(getter_AddRefs(uri)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + mimeTypeGuess.Truncate(); + break; + } + + case ExtContentPolicy::TYPE_CSP_REPORT: { + mimeTypeGuess.Truncate(); + break; + } + + case ExtContentPolicy::TYPE_XSLT: { + mimeTypeGuess = "application/xml"_ns; +#ifdef DEBUG + { + nsCOMPtr<nsINode> node = aLoadInfo->LoadingNode(); + MOZ_ASSERT(!node || node->NodeType() == nsINode::DOCUMENT_NODE, + "type_xslt requires requestingContext of type Document"); + } +#endif + break; + } + + case ExtContentPolicy::TYPE_BEACON: { + mimeTypeGuess.Truncate(); +#ifdef DEBUG + { + nsCOMPtr<nsINode> node = aLoadInfo->LoadingNode(); + MOZ_ASSERT(!node || node->NodeType() == nsINode::DOCUMENT_NODE, + "type_beacon requires requestingContext of type Document"); + } +#endif + break; + } + + case ExtContentPolicy::TYPE_FETCH: { + mimeTypeGuess.Truncate(); + break; + } + + case ExtContentPolicy::TYPE_IMAGESET: { + mimeTypeGuess.Truncate(); + break; + } + + case ExtContentPolicy::TYPE_WEB_MANIFEST: { + mimeTypeGuess = "application/manifest+json"_ns; + break; + } + + case ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD: { + mimeTypeGuess.Truncate(); + break; + } + + case ExtContentPolicy::TYPE_SPECULATIVE: { + mimeTypeGuess.Truncate(); + break; + } + + case ExtContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA: { + mimeTypeGuess.Truncate(); + break; + } + + case ExtContentPolicy::TYPE_INVALID: + MOZ_ASSERT(false, + "can not perform security check without a valid contentType"); + // Do not add default: so that compilers can catch the missing case. + } + + int16_t shouldLoad = nsIContentPolicy::ACCEPT; + rv = NS_CheckContentLoadPolicy(uri, aLoadInfo, mimeTypeGuess, &shouldLoad, + nsContentUtils::GetContentPolicy()); + + if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) { + NS_SetRequestBlockingReasonIfNull( + aLoadInfo, nsILoadInfo::BLOCKING_REASON_CONTENT_POLICY_GENERAL); + + if (NS_SUCCEEDED(rv) && + (contentPolicyType == ExtContentPolicy::TYPE_DOCUMENT || + contentPolicyType == ExtContentPolicy::TYPE_SUBDOCUMENT)) { + if (shouldLoad == nsIContentPolicy::REJECT_TYPE) { + // for docshell loads we might have to return SHOW_ALT. + return NS_ERROR_CONTENT_BLOCKED_SHOW_ALT; + } + if (shouldLoad == nsIContentPolicy::REJECT_POLICY) { + return NS_ERROR_BLOCKED_BY_POLICY; + } + } + return NS_ERROR_CONTENT_BLOCKED; + } + + return NS_OK; +} + +static void LogHTTPSOnlyInfo(nsILoadInfo* aLoadInfo) { + MOZ_LOG(sCSMLog, LogLevel::Verbose, (" httpsOnlyFirstStatus:")); + uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus(); + + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UNINITIALIZED) { + MOZ_LOG(sCSMLog, LogLevel::Verbose, (" - HTTPS_ONLY_UNINITIALIZED")); + } + if (httpsOnlyStatus & + nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_NOT_REGISTERED) { + MOZ_LOG(sCSMLog, LogLevel::Verbose, + (" - HTTPS_ONLY_UPGRADED_LISTENER_NOT_REGISTERED")); + } + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_REGISTERED) { + MOZ_LOG(sCSMLog, LogLevel::Verbose, + (" - HTTPS_ONLY_UPGRADED_LISTENER_REGISTERED")); + } + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) { + MOZ_LOG(sCSMLog, LogLevel::Verbose, (" - HTTPS_ONLY_EXEMPT")); + } + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS) { + MOZ_LOG(sCSMLog, LogLevel::Verbose, + (" - HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS")); + } + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_DOWNLOAD_IN_PROGRESS) { + MOZ_LOG(sCSMLog, LogLevel::Verbose, + (" - HTTPS_ONLY_DOWNLOAD_IN_PROGRESS")); + } + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_DO_NOT_LOG_TO_CONSOLE) { + MOZ_LOG(sCSMLog, LogLevel::Verbose, + (" - HTTPS_ONLY_DO_NOT_LOG_TO_CONSOLE")); + } + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST) { + MOZ_LOG(sCSMLog, LogLevel::Verbose, + (" - HTTPS_ONLY_UPGRADED_HTTPS_FIRST")); + } +} + +static void LogPrincipal(nsIPrincipal* aPrincipal, + const nsAString& aPrincipalName, + const uint8_t& aNestingLevel) { + nsPrintfCString aIndentationString("%*s", aNestingLevel * 2, ""); + + if (aPrincipal && aPrincipal->IsSystemPrincipal()) { + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("%s%s: SystemPrincipal\n", aIndentationString.get(), + NS_ConvertUTF16toUTF8(aPrincipalName).get())); + return; + } + if (aPrincipal) { + if (aPrincipal->GetIsNullPrincipal()) { + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("%s%s: NullPrincipal\n", aIndentationString.get(), + NS_ConvertUTF16toUTF8(aPrincipalName).get())); + return; + } + if (aPrincipal->GetIsExpandedPrincipal()) { + nsCOMPtr<nsIExpandedPrincipal> expanded(do_QueryInterface(aPrincipal)); + nsAutoCString origin; + origin.AssignLiteral("[Expanded Principal ["); + + StringJoinAppend(origin, ", "_ns, expanded->AllowList(), + [](nsACString& dest, nsIPrincipal* principal) { + nsAutoCString subOrigin; + DebugOnly<nsresult> rv = + principal->GetOrigin(subOrigin); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + dest.Append(subOrigin); + }); + + origin.AppendLiteral("]]"); + + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("%s%s: %s\n", aIndentationString.get(), + NS_ConvertUTF16toUTF8(aPrincipalName).get(), origin.get())); + return; + } + nsAutoCString principalSpec; + aPrincipal->GetAsciiSpec(principalSpec); + if (aPrincipalName.IsEmpty()) { + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("%s - \"%s\"\n", aIndentationString.get(), principalSpec.get())); + } else { + MOZ_LOG( + sCSMLog, LogLevel::Debug, + ("%s%s: \"%s\"\n", aIndentationString.get(), + NS_ConvertUTF16toUTF8(aPrincipalName).get(), principalSpec.get())); + } + return; + } + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("%s%s: nullptr\n", aIndentationString.get(), + NS_ConvertUTF16toUTF8(aPrincipalName).get())); +} + +static void LogSecurityFlags(nsSecurityFlags securityFlags) { + struct DebugSecFlagType { + unsigned long secFlag; + char secTypeStr[128]; + }; + static const DebugSecFlagType secTypes[] = { + {nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, + "SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK"}, + {nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT, + "SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT"}, + {nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED, + "SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED"}, + {nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, + "SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT"}, + {nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + "SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL"}, + {nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT, + "SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT"}, + {nsILoadInfo::SEC_COOKIES_DEFAULT, "SEC_COOKIES_DEFAULT"}, + {nsILoadInfo::SEC_COOKIES_INCLUDE, "SEC_COOKIES_INCLUDE"}, + {nsILoadInfo::SEC_COOKIES_SAME_ORIGIN, "SEC_COOKIES_SAME_ORIGIN"}, + {nsILoadInfo::SEC_COOKIES_OMIT, "SEC_COOKIES_OMIT"}, + {nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL, "SEC_FORCE_INHERIT_PRINCIPAL"}, + {nsILoadInfo::SEC_ABOUT_BLANK_INHERITS, "SEC_ABOUT_BLANK_INHERITS"}, + {nsILoadInfo::SEC_ALLOW_CHROME, "SEC_ALLOW_CHROME"}, + {nsILoadInfo::SEC_DISALLOW_SCRIPT, "SEC_DISALLOW_SCRIPT"}, + {nsILoadInfo::SEC_DONT_FOLLOW_REDIRECTS, "SEC_DONT_FOLLOW_REDIRECTS"}, + {nsILoadInfo::SEC_LOAD_ERROR_PAGE, "SEC_LOAD_ERROR_PAGE"}, + {nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL_OVERRULE_OWNER, + "SEC_FORCE_INHERIT_PRINCIPAL_OVERRULE_OWNER"}}; + + for (const DebugSecFlagType& flag : secTypes) { + if (securityFlags & flag.secFlag) { + // the logging level should be in sync with the logging level in + // DebugDoContentSecurityCheck() + MOZ_LOG(sCSMLog, LogLevel::Verbose, (" - %s\n", flag.secTypeStr)); + } + } +} +static void DebugDoContentSecurityCheck(nsIChannel* aChannel, + nsILoadInfo* aLoadInfo) { + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aChannel)); + + MOZ_LOG(sCSMLog, LogLevel::Debug, ("\n#DebugDoContentSecurityCheck Begin\n")); + + // we only log http channels, unless loglevel is 5. + if (httpChannel || MOZ_LOG_TEST(sCSMLog, LogLevel::Verbose)) { + MOZ_LOG(sCSMLog, LogLevel::Verbose, ("doContentSecurityCheck:\n")); + + nsAutoCString remoteType; + if (XRE_IsParentProcess()) { + nsCOMPtr<nsIParentChannel> parentChannel; + NS_QueryNotificationCallbacks(aChannel, parentChannel); + if (parentChannel) { + parentChannel->GetRemoteType(remoteType); + } + } else { + remoteType.Assign( + mozilla::dom::ContentChild::GetSingleton()->GetRemoteType()); + } + MOZ_LOG(sCSMLog, LogLevel::Verbose, + (" processType: \"%s\"\n", remoteType.get())); + + nsCOMPtr<nsIURI> channelURI; + nsAutoCString channelSpec; + nsAutoCString channelMethod; + NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI)); + if (channelURI) { + channelURI->GetSpec(channelSpec); + } + MOZ_LOG(sCSMLog, LogLevel::Verbose, + (" channelURI: \"%s\"\n", channelSpec.get())); + + // Log HTTP-specific things + if (httpChannel) { + nsresult rv; + rv = httpChannel->GetRequestMethod(channelMethod); + if (!NS_FAILED(rv)) { + MOZ_LOG(sCSMLog, LogLevel::Verbose, + (" httpMethod: %s\n", channelMethod.get())); + } + } + + // Log Principals + nsCOMPtr<nsIPrincipal> requestPrincipal = aLoadInfo->TriggeringPrincipal(); + LogPrincipal(aLoadInfo->GetLoadingPrincipal(), u"loadingPrincipal"_ns, 1); + LogPrincipal(requestPrincipal, u"triggeringPrincipal"_ns, 1); + LogPrincipal(aLoadInfo->PrincipalToInherit(), u"principalToInherit"_ns, 1); + + // Log Redirect Chain + MOZ_LOG(sCSMLog, LogLevel::Verbose, (" redirectChain:\n")); + for (nsIRedirectHistoryEntry* redirectHistoryEntry : + aLoadInfo->RedirectChain()) { + nsCOMPtr<nsIPrincipal> principal; + redirectHistoryEntry->GetPrincipal(getter_AddRefs(principal)); + LogPrincipal(principal, u""_ns, 2); + } + + MOZ_LOG(sCSMLog, LogLevel::Verbose, + (" internalContentPolicyType: %s\n", + NS_CP_ContentTypeName(aLoadInfo->InternalContentPolicyType()))); + MOZ_LOG(sCSMLog, LogLevel::Verbose, + (" externalContentPolicyType: %s\n", + NS_CP_ContentTypeName(aLoadInfo->GetExternalContentPolicyType()))); + MOZ_LOG(sCSMLog, LogLevel::Verbose, + (" upgradeInsecureRequests: %s\n", + aLoadInfo->GetUpgradeInsecureRequests() ? "true" : "false")); + MOZ_LOG(sCSMLog, LogLevel::Verbose, + (" initialSecurityChecksDone: %s\n", + aLoadInfo->GetInitialSecurityCheckDone() ? "true" : "false")); + MOZ_LOG(sCSMLog, LogLevel::Verbose, + (" allowDeprecatedSystemRequests: %s\n", + aLoadInfo->GetAllowDeprecatedSystemRequests() ? "true" : "false")); + + // Log CSPrequestPrincipal + nsCOMPtr<nsIContentSecurityPolicy> csp = aLoadInfo->GetCsp(); + MOZ_LOG(sCSMLog, LogLevel::Debug, (" CSP:")); + if (csp) { + nsAutoString parsedPolicyStr; + uint32_t count = 0; + csp->GetPolicyCount(&count); + for (uint32_t i = 0; i < count; ++i) { + csp->GetPolicyString(i, parsedPolicyStr); + // we need to add quotation marks, as otherwise yaml parsers may fail + // with CSP directives + // no need to escape quote marks in the parsed policy string, as URLs in + // there are already encoded + MOZ_LOG(sCSMLog, LogLevel::Debug, + (" - \"%s\"\n", NS_ConvertUTF16toUTF8(parsedPolicyStr).get())); + } + } + + // Security Flags + MOZ_LOG(sCSMLog, LogLevel::Verbose, (" securityFlags:")); + LogSecurityFlags(aLoadInfo->GetSecurityFlags()); + LogHTTPSOnlyInfo(aLoadInfo); + MOZ_LOG(sCSMLog, LogLevel::Debug, ("\n#DebugDoContentSecurityCheck End\n")); + } +} + +/* static */ +void nsContentSecurityManager::MeasureUnexpectedPrivilegedLoads( + nsILoadInfo* aLoadInfo, nsIURI* aFinalURI, const nsACString& aRemoteType) { + if (!StaticPrefs::dom_security_unexpected_system_load_telemetry_enabled()) { + return; + } + nsContentSecurityUtils::DetectJsHacks(); + nsContentSecurityUtils::DetectCssHacks(); + // The detection only work on the main-thread. + // To avoid races and early reports, we need to ensure the checks actually + // happened. + if (MOZ_UNLIKELY(sJSHacksPresent || !sJSHacksChecked || sCSSHacksPresent || + !sCSSHacksChecked)) { + return; + } + + ExtContentPolicyType contentPolicyType = + aLoadInfo->GetExternalContentPolicyType(); + // restricting reported types to script, styles and documents + // to be continued in follow-ups of bug 1697163. + if (contentPolicyType != ExtContentPolicyType::TYPE_SCRIPT && + contentPolicyType != ExtContentPolicyType::TYPE_STYLESHEET && + contentPolicyType != ExtContentPolicyType::TYPE_DOCUMENT) { + return; + } + + // Gather redirected schemes in string + nsAutoCString loggedRedirects; + const nsTArray<nsCOMPtr<nsIRedirectHistoryEntry>>& redirects = + aLoadInfo->RedirectChain(); + if (!redirects.IsEmpty()) { + nsCOMPtr<nsIRedirectHistoryEntry> end = redirects.LastElement(); + for (nsIRedirectHistoryEntry* entry : redirects) { + nsCOMPtr<nsIPrincipal> principal; + entry->GetPrincipal(getter_AddRefs(principal)); + if (principal) { + nsAutoCString scheme; + principal->GetScheme(scheme); + loggedRedirects.Append(scheme); + if (entry != end) { + loggedRedirects.AppendLiteral(", "); + } + } + } + } + + nsAutoCString uriString; + if (aFinalURI) { + aFinalURI->GetAsciiSpec(uriString); + } else { + uriString.AssignLiteral(""); + } + FilenameTypeAndDetails fileNameTypeAndDetails = + nsContentSecurityUtils::FilenameToFilenameType( + NS_ConvertUTF8toUTF16(uriString), true); + + nsCString loggedFileDetails = "unknown"_ns; + if (fileNameTypeAndDetails.second.isSome()) { + loggedFileDetails.Assign( + NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value())); + } + // sanitize remoteType because it may contain sensitive + // info, like URLs. e.g. `webIsolated=https://example.com` + nsAutoCString loggedRemoteType(dom::RemoteTypePrefix(aRemoteType)); + nsAutoCString loggedContentType(NS_CP_ContentTypeName(contentPolicyType)); + + MOZ_LOG(sCSMLog, LogLevel::Debug, ("UnexpectedPrivilegedLoadTelemetry:\n")); + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("- contentType: %s\n", loggedContentType.get())); + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("- URL (not to be reported): %s\n", uriString.get())); + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("- remoteType: %s\n", loggedRemoteType.get())); + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("- fileInfo: %s\n", fileNameTypeAndDetails.first.get())); + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("- fileDetails: %s\n", loggedFileDetails.get())); + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("- redirects: %s\n\n", loggedRedirects.get())); + + // Send Telemetry + auto extra = Some<nsTArray<EventExtraEntry>>( + {EventExtraEntry{"contenttype"_ns, loggedContentType}, + EventExtraEntry{"remotetype"_ns, loggedRemoteType}, + EventExtraEntry{"filedetails"_ns, loggedFileDetails}, + EventExtraEntry{"redirects"_ns, loggedRedirects}}); + + if (!sTelemetryEventEnabled.exchange(true)) { + Telemetry::SetEventRecordingEnabled("security"_ns, true); + } + + Telemetry::EventID eventType = + Telemetry::EventID::Security_Unexpectedload_Systemprincipal; + Telemetry::RecordEvent(eventType, mozilla::Some(fileNameTypeAndDetails.first), + extra); +} + +/* static */ +nsSecurityFlags nsContentSecurityManager::ComputeSecurityFlags( + mozilla::CORSMode aCORSMode, CORSSecurityMapping aCORSSecurityMapping) { + if (aCORSSecurityMapping == CORSSecurityMapping::DISABLE_CORS_CHECKS) { + return nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL; + } + + switch (aCORSMode) { + case CORS_NONE: + if (aCORSSecurityMapping == CORSSecurityMapping::REQUIRE_CORS_CHECKS) { + // CORS_NONE gets treated like CORS_ANONYMOUS in this mode + return nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT | + nsILoadInfo::SEC_COOKIES_SAME_ORIGIN; + } else if (aCORSSecurityMapping == + CORSSecurityMapping::CORS_NONE_MAPS_TO_INHERITED_CONTEXT) { + // CORS_NONE inherits + return nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT; + } else { + // CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS, the only remaining enum + // variant. CORSSecurityMapping::DISABLE_CORS_CHECKS returned early. + MOZ_ASSERT(aCORSSecurityMapping == + CORSSecurityMapping::CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS); + return nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL; + } + case CORS_ANONYMOUS: + return nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT | + nsILoadInfo::SEC_COOKIES_SAME_ORIGIN; + case CORS_USE_CREDENTIALS: + return nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT | + nsILoadInfo::SEC_COOKIES_INCLUDE; + break; + default: + MOZ_ASSERT_UNREACHABLE("Invalid aCORSMode enum value"); + return nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT | + nsILoadInfo::SEC_COOKIES_SAME_ORIGIN; + } +} + +/* static */ +nsresult nsContentSecurityManager::CheckAllowLoadInSystemPrivilegedContext( + nsIChannel* aChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + nsCOMPtr<nsIPrincipal> inspectedPrincipal = loadInfo->GetLoadingPrincipal(); + if (!inspectedPrincipal) { + return NS_OK; + } + // Check if we are actually dealing with a privileged request + if (!inspectedPrincipal->IsSystemPrincipal()) { + return NS_OK; + } + // loads with the allow flag are waived through + // until refactored (e.g., Shavar, OCSP) + if (loadInfo->GetAllowDeprecatedSystemRequests()) { + return NS_OK; + } + ExtContentPolicyType contentPolicyType = + loadInfo->GetExternalContentPolicyType(); + // For now, let's not inspect top-level document loads + if (contentPolicyType == ExtContentPolicy::TYPE_DOCUMENT) { + return NS_OK; + } + + // allowing some fetches due to their lowered risk + // i.e., data & downloads fetches do limited parsing, no rendering + // remote images are too widely used (favicons, about:addons etc.) + if ((contentPolicyType == ExtContentPolicy::TYPE_FETCH) || + (contentPolicyType == ExtContentPolicy::TYPE_XMLHTTPREQUEST) || + (contentPolicyType == ExtContentPolicy::TYPE_WEBSOCKET) || + (contentPolicyType == ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD) || + (contentPolicyType == ExtContentPolicy::TYPE_IMAGE)) { + return NS_OK; + } + + // Allow the user interface (e.g., schemes like chrome, resource) + nsCOMPtr<nsIURI> finalURI; + NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI)); + bool isUiResource = false; + if (NS_SUCCEEDED(NS_URIChainHasFlags( + finalURI, nsIProtocolHandler::URI_IS_UI_RESOURCE, &isUiResource)) && + isUiResource) { + return NS_OK; + } + // For about: and extension-based URIs, which don't get + // URI_IS_UI_RESOURCE, first remove layers of view-source:, if present. + nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(finalURI); + + nsAutoCString remoteType; + if (XRE_IsParentProcess()) { + nsCOMPtr<nsIParentChannel> parentChannel; + NS_QueryNotificationCallbacks(aChannel, parentChannel); + if (parentChannel) { + parentChannel->GetRemoteType(remoteType); + } + } else { + remoteType.Assign( + mozilla::dom::ContentChild::GetSingleton()->GetRemoteType()); + } + + // GetInnerURI can return null for malformed nested URIs like moz-icon:trash + if (!innerURI) { + MeasureUnexpectedPrivilegedLoads(loadInfo, innerURI, remoteType); + if (StaticPrefs::security_disallow_privileged_no_finaluri_loads()) { + aChannel->Cancel(NS_ERROR_CONTENT_BLOCKED); + return NS_ERROR_CONTENT_BLOCKED; + } + return NS_OK; + } + // loads of userContent.css during startup and tests that show up as file: + if (innerURI->SchemeIs("file")) { + if ((contentPolicyType == ExtContentPolicy::TYPE_STYLESHEET) || + (contentPolicyType == ExtContentPolicy::TYPE_OTHER)) { + return NS_OK; + } + } + // (1) loads from within omni.ja and system add-ons use jar: + // this is safe to allow, because we do not support remote jar. + // (2) about: resources are always allowed: they are part of the build. + // (3) extensions are signed or the user has made bad decisions. + if (innerURI->SchemeIs("jar") || innerURI->SchemeIs("about") || + innerURI->SchemeIs("moz-extension")) { + return NS_OK; + } + + nsAutoCString requestedURL; + innerURI->GetAsciiSpec(requestedURL); + MOZ_LOG(sCSMLog, LogLevel::Warning, + ("SystemPrincipal should not load remote resources. URL: %s, type %d", + requestedURL.get(), int(contentPolicyType))); + + // The load types that we want to disallow, will extend over time and + // prioritized by risk. The most risky/dangerous are load-types are documents, + // subdocuments, scripts and styles in that order. The most dangerous URL + // schemes to cover are HTTP, HTTPS, data, blob in that order. Meta bug + // 1725112 will track upcoming restrictions + + // Telemetry for unexpected privileged loads. + // pref check & data sanitization happens in the called function + MeasureUnexpectedPrivilegedLoads(loadInfo, innerURI, remoteType); + + // Relaxing restrictions for our test suites: + // (1) AreNonLocalConnectionsDisabled() disables network, so + // http://mochitest is actually local and allowed. (2) The marionette test + // framework uses injections and data URLs to execute scripts, checking for + // the environment variable breaks the attack but not the tests. + if (xpc::AreNonLocalConnectionsDisabled() || + mozilla::EnvHasValue("MOZ_MARIONETTE")) { + bool disallowSystemPrincipalRemoteDocuments = Preferences::GetBool( + "security.disallow_non_local_systemprincipal_in_tests"); + if (disallowSystemPrincipalRemoteDocuments) { + // our own mochitest needs NS_ASSERTION instead of MOZ_ASSERT + NS_ASSERTION(false, "SystemPrincipal must not load remote documents."); + aChannel->Cancel(NS_ERROR_CONTENT_BLOCKED); + return NS_ERROR_CONTENT_BLOCKED; + } + // but other mochitest are exempt from this + return NS_OK; + } + + if (contentPolicyType == ExtContentPolicy::TYPE_SUBDOCUMENT) { + if (StaticPrefs::security_disallow_privileged_https_subdocuments_loads() && + (innerURI->SchemeIs("http") || innerURI->SchemeIs("https"))) { + MOZ_ASSERT( + false, + "Disallowing SystemPrincipal load of subdocuments on HTTP(S)."); + aChannel->Cancel(NS_ERROR_CONTENT_BLOCKED); + return NS_ERROR_CONTENT_BLOCKED; + } + if ((StaticPrefs::security_disallow_privileged_data_subdocuments_loads()) && + (innerURI->SchemeIs("data"))) { + MOZ_ASSERT( + false, + "Disallowing SystemPrincipal load of subdocuments on data URL."); + aChannel->Cancel(NS_ERROR_CONTENT_BLOCKED); + return NS_ERROR_CONTENT_BLOCKED; + } + } + if (contentPolicyType == ExtContentPolicy::TYPE_SCRIPT) { + if ((StaticPrefs::security_disallow_privileged_https_script_loads()) && + (innerURI->SchemeIs("http") || innerURI->SchemeIs("https"))) { + MOZ_ASSERT(false, + "Disallowing SystemPrincipal load of scripts on HTTP(S)."); + aChannel->Cancel(NS_ERROR_CONTENT_BLOCKED); + return NS_ERROR_CONTENT_BLOCKED; + } + } + if (contentPolicyType == ExtContentPolicy::TYPE_STYLESHEET) { + if (StaticPrefs::security_disallow_privileged_https_stylesheet_loads() && + (innerURI->SchemeIs("http") || innerURI->SchemeIs("https"))) { + MOZ_ASSERT(false, + "Disallowing SystemPrincipal load of stylesheets on HTTP(S)."); + aChannel->Cancel(NS_ERROR_CONTENT_BLOCKED); + return NS_ERROR_CONTENT_BLOCKED; + } + } + return NS_OK; +} + +/* + * Disallow about pages in the privilegedaboutcontext (e.g., password manager, + * newtab etc.) to load remote scripts. Regardless of whether this is coming + * from the contentprincipal or the systemprincipal. + */ +/* static */ +nsresult nsContentSecurityManager::CheckAllowLoadInPrivilegedAboutContext( + nsIChannel* aChannel) { + // bail out if check is disabled + if (StaticPrefs::security_disallow_privilegedabout_remote_script_loads()) { + return NS_OK; + } + + nsAutoCString remoteType; + if (XRE_IsParentProcess()) { + nsCOMPtr<nsIParentChannel> parentChannel; + NS_QueryNotificationCallbacks(aChannel, parentChannel); + if (parentChannel) { + parentChannel->GetRemoteType(remoteType); + } + } else { + remoteType.Assign( + mozilla::dom::ContentChild::GetSingleton()->GetRemoteType()); + } + + // only perform check for privileged about process + if (!remoteType.Equals(PRIVILEGEDABOUT_REMOTE_TYPE)) { + return NS_OK; + } + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + ExtContentPolicyType contentPolicyType = + loadInfo->GetExternalContentPolicyType(); + // only check for script loads + if (contentPolicyType != ExtContentPolicy::TYPE_SCRIPT) { + return NS_OK; + } + + nsCOMPtr<nsIURI> finalURI; + NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI)); + nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(finalURI); + + bool isLocal; + NS_URIChainHasFlags(innerURI, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE, + &isLocal); + // We allow URLs that are URI_IS_LOCAL (but that includes `data` + // and `blob` which are also undesirable. + if ((isLocal) && (!innerURI->SchemeIs("data")) && + (!innerURI->SchemeIs("blob"))) { + return NS_OK; + } + MOZ_ASSERT( + false, + "Disallowing privileged about process to load scripts on HTTP(S)."); + aChannel->Cancel(NS_ERROR_CONTENT_BLOCKED); + return NS_ERROR_CONTENT_BLOCKED; +} + +/* + * Every protocol handler must set one of the six security flags + * defined in nsIProtocolHandler - if not - deny the load. + */ +nsresult nsContentSecurityManager::CheckChannelHasProtocolSecurityFlag( + nsIChannel* aChannel) { + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIIOService> ios = do_GetIOService(&rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t flags; + rv = ios->GetDynamicProtocolFlags(uri, &flags); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t securityFlagsSet = 0; + if (flags & nsIProtocolHandler::WEBEXT_URI_WEB_ACCESSIBLE) { + securityFlagsSet += 1; + } + if (flags & nsIProtocolHandler::URI_LOADABLE_BY_ANYONE) { + securityFlagsSet += 1; + } + if (flags & nsIProtocolHandler::URI_DANGEROUS_TO_LOAD) { + securityFlagsSet += 1; + } + if (flags & nsIProtocolHandler::URI_IS_UI_RESOURCE) { + securityFlagsSet += 1; + } + if (flags & nsIProtocolHandler::URI_IS_LOCAL_FILE) { + securityFlagsSet += 1; + } + if (flags & nsIProtocolHandler::URI_LOADABLE_BY_SUBSUMERS) { + securityFlagsSet += 1; + } + + // Ensure that only "1" valid security flags is set. + if (securityFlagsSet == 1) { + return NS_OK; + } + + MOZ_ASSERT(false, "protocol must use one valid security flag"); + return NS_ERROR_CONTENT_BLOCKED; +} + +// We should not allow loading non-JavaScript files as scripts using +// a file:// URL. +static nsresult CheckAllowFileProtocolScriptLoad(nsIChannel* aChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + ExtContentPolicyType type = loadInfo->GetExternalContentPolicyType(); + + // Only check script loads. + if (type != ExtContentPolicy::TYPE_SCRIPT) { + return NS_OK; + } + + if (!StaticPrefs::security_block_fileuri_script_with_wrong_mime()) { + return NS_OK; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + if (!uri || !uri->SchemeIs("file")) { + return NS_OK; + } + + nsCOMPtr<nsIMIMEService> mime = do_GetService("@mozilla.org/mime;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // GetTypeFromURI fails for missing or unknown file-extensions. + nsAutoCString contentType; + rv = mime->GetTypeFromURI(uri, contentType); + if (NS_FAILED(rv) || !nsContentUtils::IsJavascriptMIMEType( + NS_ConvertUTF8toUTF16(contentType))) { + nsCOMPtr<Document> doc; + if (nsINode* node = loadInfo->LoadingNode()) { + doc = node->OwnerDoc(); + } + + nsAutoCString spec; + uri->GetSpec(spec); + + AutoTArray<nsString, 1> params; + CopyUTF8toUTF16(NS_UnescapeURL(spec), *params.AppendElement()); + CopyUTF8toUTF16(NS_UnescapeURL(contentType), *params.AppendElement()); + + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + "FILE_SCRIPT_BLOCKED"_ns, doc, + nsContentUtils::eSECURITY_PROPERTIES, + "BlockFileScriptWithWrongMimeType", params); + + return NS_ERROR_CONTENT_BLOCKED; + } + + return NS_OK; +} + +// We should not allow loading non-JavaScript files as scripts using +// a moz-extension:// URL. +static nsresult CheckAllowExtensionProtocolScriptLoad(nsIChannel* aChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + ExtContentPolicyType type = loadInfo->GetExternalContentPolicyType(); + + // Only check script loads. + if (type != ExtContentPolicy::TYPE_SCRIPT) { + return NS_OK; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + if (!uri || !uri->SchemeIs("moz-extension")) { + return NS_OK; + } + + // We expect this code to never be hit off-the-main-thread (even worker + // scripts are currently hitting only on the main thread, see + // WorkerScriptLoader::DispatchLoadScript calling NS_DispatchToMainThread + // internally), this diagnostic assertion is meant to let us notice if that + // isn't the case anymore. + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(), + "Unexpected off-the-main-thread call to " + "CheckAllowFileProtocolScriptLoad"); + + nsAutoCString host; + rv = uri->GetHost(host); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<extensions::WebExtensionPolicyCore> targetPolicy = + ExtensionPolicyService::GetCoreByHost(host); + + if (NS_WARN_IF(!targetPolicy) || targetPolicy->ManifestVersion() < 3) { + return NS_OK; + } + + nsCOMPtr<nsIMIMEService> mime = do_GetService("@mozilla.org/mime;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // GetDefaultTypeFromExtension fails for missing or unknown file-extensions. + nsAutoCString contentType; + rv = mime->GetDefaultTypeFromURI(uri, contentType); + if (NS_FAILED(rv) || !nsContentUtils::IsJavascriptMIMEType( + NS_ConvertUTF8toUTF16(contentType))) { + nsCOMPtr<Document> doc; + if (nsINode* node = loadInfo->LoadingNode()) { + doc = node->OwnerDoc(); + } + + nsAutoCString spec; + uri->GetSpec(spec); + + AutoTArray<nsString, 1> params; + CopyUTF8toUTF16(NS_UnescapeURL(spec), *params.AppendElement()); + + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + "EXTENSION_SCRIPT_BLOCKED"_ns, doc, + nsContentUtils::eSECURITY_PROPERTIES, + "BlockExtensionScriptWithWrongExt", params); + + return NS_ERROR_CONTENT_BLOCKED; + } + + return NS_OK; +} + +// Validate that a load should be allowed based on its remote type. This +// intentionally prevents some loads from occuring even using the system +// principal, if they were started in a content process. +static nsresult CheckAllowLoadByTriggeringRemoteType(nsIChannel* aChannel) { + MOZ_ASSERT(aChannel); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + // For now, only restrict loads for documents. We currently have no + // interesting subresource checks for protocols which are are not fully + // handled within the content process. + ExtContentPolicy contentPolicyType = loadInfo->GetExternalContentPolicyType(); + if (contentPolicyType != ExtContentPolicy::TYPE_DOCUMENT && + contentPolicyType != ExtContentPolicy::TYPE_SUBDOCUMENT && + contentPolicyType != ExtContentPolicy::TYPE_OBJECT) { + return NS_OK; + } + + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(), + "Unexpected off-the-main-thread call to " + "CheckAllowLoadByTriggeringRemoteType"); + + // Due to the way that session history is handled without SHIP, we cannot run + // these checks when SHIP is disabled. + if (!mozilla::SessionHistoryInParent()) { + return NS_OK; + } + + nsAutoCString triggeringRemoteType; + nsresult rv = loadInfo->GetTriggeringRemoteType(triggeringRemoteType); + NS_ENSURE_SUCCESS(rv, rv); + + // For now, only restrict loads coming from web remote types. In the future we + // may want to expand this a bit. + if (!StringBeginsWith(triggeringRemoteType, WEB_REMOTE_TYPE)) { + return NS_OK; + } + + nsCOMPtr<nsIURI> finalURI; + rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI)); + NS_ENSURE_SUCCESS(rv, rv); + + // Don't allow web content processes to load non-remote about pages. + // NOTE: URIs with a `moz-safe-about:` inner scheme are safe to link to, so + // it's OK we miss them here. + nsCOMPtr<nsIURI> innermostURI = NS_GetInnermostURI(finalURI); + if (innermostURI->SchemeIs("about")) { + nsCOMPtr<nsIAboutModule> aboutModule; + rv = NS_GetAboutModule(innermostURI, getter_AddRefs(aboutModule)); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t aboutModuleFlags = 0; + rv = aboutModule->GetURIFlags(innermostURI, &aboutModuleFlags); + NS_ENSURE_SUCCESS(rv, rv); + + if (!(aboutModuleFlags & nsIAboutModule::MAKE_LINKABLE) && + !(aboutModuleFlags & nsIAboutModule::URI_CAN_LOAD_IN_CHILD) && + !(aboutModuleFlags & nsIAboutModule::URI_MUST_LOAD_IN_CHILD)) { + NS_WARNING(nsPrintfCString("Blocking load of about URI (%s) which cannot " + "be linked to in web content process", + finalURI->GetSpecOrDefault().get()) + .get()); +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + if (NS_SUCCEEDED( + loadInfo->TriggeringPrincipal()->CheckMayLoad(finalURI, true))) { + nsAutoCString aboutModuleName; + MOZ_ALWAYS_SUCCEEDS( + NS_GetAboutModuleName(innermostURI, aboutModuleName)); + MOZ_CRASH_UNSAFE_PRINTF( + "Blocking load of about uri by content process which may have " + "otherwise succeeded [aboutModule=%s, isSystemPrincipal=%d]", + aboutModuleName.get(), + loadInfo->TriggeringPrincipal()->IsSystemPrincipal()); + } +#endif + return NS_ERROR_CONTENT_BLOCKED; + } + return NS_OK; + } + + // Don't allow web content processes to load file documents. Loads of file + // URIs as subresources will be handled by the sandbox, and may be allowed in + // some cases. + bool localFile = false; + rv = NS_URIChainHasFlags(finalURI, nsIProtocolHandler::URI_IS_LOCAL_FILE, + &localFile); + NS_ENSURE_SUCCESS(rv, rv); + if (localFile) { + NS_WARNING( + nsPrintfCString( + "Blocking document load of file URI (%s) from web content process", + innermostURI->GetSpecOrDefault().get()) + .get()); +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + if (NS_SUCCEEDED( + loadInfo->TriggeringPrincipal()->CheckMayLoad(finalURI, true))) { + MOZ_CRASH_UNSAFE_PRINTF( + "Blocking document load of file URI by content process which may " + "have otherwise succeeded [isSystemPrincipal=%d]", + loadInfo->TriggeringPrincipal()->IsSystemPrincipal()); + } +#endif + return NS_ERROR_CONTENT_BLOCKED; + } + + 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->AsyncOpen() 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->LoadInfo(); + if (MOZ_UNLIKELY(MOZ_LOG_TEST(sCSMLog, LogLevel::Verbose))) { + DebugDoContentSecurityCheck(aChannel, loadInfo); + } + + nsresult rv = CheckAllowLoadInSystemPrivilegedContext(aChannel); + NS_ENSURE_SUCCESS(rv, rv); + + rv = CheckAllowLoadInPrivilegedAboutContext(aChannel); + NS_ENSURE_SUCCESS(rv, rv); + + // We want to also check redirected requests to ensure + // the target maintains the proper javascript file extensions. + rv = CheckAllowExtensionProtocolScriptLoad(aChannel); + NS_ENSURE_SUCCESS(rv, rv); + + rv = CheckChannelHasProtocolSecurityFlag(aChannel); + NS_ENSURE_SUCCESS(rv, rv); + + rv = CheckAllowLoadByTriggeringRemoteType(aChannel); + NS_ENSURE_SUCCESS(rv, rv); + + // 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 + rv = ValidateSecurityFlags(loadInfo); + NS_ENSURE_SUCCESS(rv, rv); + + if (loadInfo->GetSecurityMode() == + nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) { + 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); + + // Apply this after CSP to match Chrome. + rv = CheckFTPSubresourceLoad(aChannel); + NS_ENSURE_SUCCESS(rv, rv); + + rv = CheckAllowFileProtocolScriptLoad(aChannel); + NS_ENSURE_SUCCESS(rv, rv); + + // now lets set the initialSecurityFlag 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) { + // Since we compare the principal from the loadInfo to the URI's + // princicpal, it's possible that the checks fail when doing an internal + // redirect. We can just return early instead, since we should never + // need to block an internal redirect. + if (aRedirFlags & nsIChannelEventSink::REDIRECT_INTERNAL) { + aCb->OnRedirectVerifyCallback(NS_OK); + return NS_OK; + } + + nsCOMPtr<nsILoadInfo> loadInfo = aOldChannel->LoadInfo(); + nsresult rv = CheckChannel(aNewChannel); + if (NS_SUCCEEDED(rv)) { + rv = CheckFTPSubresourceLoad(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; + Unused << NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(newURI)); + NS_ENSURE_STATE(oldPrincipal && newURI); + + // Do not allow insecure redirects to data: URIs + if (!AllowInsecureRedirectToDataURI(aNewChannel)) { + // cancel the old channel and return an error + aOldChannel->Cancel(NS_ERROR_CONTENT_BLOCKED); + return NS_ERROR_CONTENT_BLOCKED; + } + + const uint32_t flags = + nsIScriptSecurityManager::LOAD_IS_AUTOMATIC_DOCUMENT_REPLACEMENT | + nsIScriptSecurityManager::DISALLOW_SCRIPT; + rv = nsContentUtils::GetSecurityManager()->CheckLoadURIWithPrincipal( + oldPrincipal, newURI, flags, loadInfo->GetInnerWindowID()); + 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->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() != + ExtContentPolicy::TYPE_DOCUMENT); + nsIPrincipal* loadingPrincipal = loadInfo->GetLoadingPrincipal(); + + // It doesn't matter what we pass for the second, data-inherits, argument. + // Any protocol which inherits won't pay attention to cookies anyway. + rv = loadingPrincipal->CheckMayLoad(uri, false); + if (NS_FAILED(rv)) { + AddLoadFlags(aChannel, nsIRequest::LOAD_ANONYMOUS); + } + } else if (cookiePolicy == nsILoadInfo::SEC_COOKIES_OMIT) { + AddLoadFlags(aChannel, nsIRequest::LOAD_ANONYMOUS); + } + + if (!CrossOriginEmbedderPolicyAllowsCredentials(aChannel)) { + AddLoadFlags(aChannel, nsIRequest::LOAD_ANONYMOUS); + } + + nsSecurityFlags securityMode = loadInfo->GetSecurityMode(); + + // CORS mode is handled by nsCORSListenerProxy + if (securityMode == nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) { + if (NS_HasBeenCrossOrigin(aChannel)) { + loadInfo->MaybeIncreaseTainting(LoadTainting::CORS); + } + return NS_OK; + } + + // Allow subresource loads if TriggeringPrincipal is the SystemPrincipal. + if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal() && + loadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT && + loadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::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_INHERITS_SEC_CONTEXT) || + (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_INHERITS_SEC_CONTEXT) || + (securityMode == + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL)) { + if (NS_HasBeenCrossOrigin(aChannel)) { + NS_ENSURE_FALSE(loadInfo->GetDontFollowRedirects(), NS_ERROR_DOM_BAD_URI); + loadInfo->MaybeIncreaseTainting(LoadTainting::Opaque); + } + // Please note that DoCheckLoadURIChecks should only be enforced for + // cross origin requests. If the flag SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT + // is set within the loadInfo, then CheckLoadURIWithPrincipal is performed + // within nsCorsListenerProxy + rv = DoCheckLoadURIChecks(uri, loadInfo); + NS_ENSURE_SUCCESS(rv, rv); + // TODO: Bug 1371237 + // consider calling SetBlockedRequest in + // nsContentSecurityManager::CheckChannel + } + + return NS_OK; +} + +// https://fetch.spec.whatwg.org/#ref-for-cross-origin-embedder-policy-allows-credentials +bool nsContentSecurityManager::CrossOriginEmbedderPolicyAllowsCredentials( + nsIChannel* aChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + // 1. If request’s mode is not "no-cors", then return true. + // + // `no-cors` check applies to document navigation such that if it is + // an document navigation, this check should return true to allow + // credentials. + if (loadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_DOCUMENT || + loadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_SUBDOCUMENT) { + return true; + } + + if (loadInfo->GetSecurityMode() != + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL && + loadInfo->GetSecurityMode() != + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT) { + return true; + } + + // If request’s client’s policy container’s embedder policy’s value is not + // "credentialless", then return true. + if (loadInfo->GetLoadingEmbedderPolicy() != + nsILoadInfo::EMBEDDER_POLICY_CREDENTIALLESS) { + return true; + } + + // If request’s origin is same origin with request’s current URL’s origin and + // request does not have a redirect-tainted origin, then return true. + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + nsCOMPtr<nsIPrincipal> resourcePrincipal; + ssm->GetChannelURIPrincipal(aChannel, getter_AddRefs(resourcePrincipal)); + + bool sameOrigin = resourcePrincipal->Equals(loadInfo->TriggeringPrincipal()); + nsAutoCString serializedOrigin; + GetSerializedOrigin(loadInfo->TriggeringPrincipal(), resourcePrincipal, + serializedOrigin, loadInfo); + if (sameOrigin && !serializedOrigin.IsEmpty()) { + return true; + } + + return false; +} + +// https://fetch.spec.whatwg.org/#serializing-a-request-origin +void nsContentSecurityManager::GetSerializedOrigin( + nsIPrincipal* aOrigin, nsIPrincipal* aResourceOrigin, + nsACString& aSerializedOrigin, nsILoadInfo* aLoadInfo) { + // The following for loop performs the + // https://fetch.spec.whatwg.org/#ref-for-concept-request-tainted-origin + nsCOMPtr<nsIPrincipal> lastOrigin; + for (nsIRedirectHistoryEntry* entry : aLoadInfo->RedirectChain()) { + if (!lastOrigin) { + entry->GetPrincipal(getter_AddRefs(lastOrigin)); + continue; + } + + nsCOMPtr<nsIPrincipal> currentOrigin; + entry->GetPrincipal(getter_AddRefs(currentOrigin)); + + if (!currentOrigin->Equals(lastOrigin) && !lastOrigin->Equals(aOrigin)) { + return; + } + lastOrigin = currentOrigin; + } + + // When the redirectChain is empty, it means this is the first redirect. + // So according to the #serializing-a-request-origin spec, we don't + // have a redirect-tainted origin, so we return the origin of the request + // here. + if (!lastOrigin) { + aOrigin->GetAsciiOrigin(aSerializedOrigin); + return; + } + + // Same as above, redirectChain doesn't contain the current redirect, + // so we have to do the check one last time here. + if (lastOrigin->Equals(aResourceOrigin) && !lastOrigin->Equals(aOrigin)) { + return; + } + + aOrigin->GetAsciiOrigin(aSerializedOrigin); +} + +// ==== 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; +} diff --git a/dom/security/nsContentSecurityManager.h b/dom/security/nsContentSecurityManager.h new file mode 100644 index 0000000000..208b52d6e5 --- /dev/null +++ b/dom/security/nsContentSecurityManager.h @@ -0,0 +1,89 @@ +/* -*- 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 "mozilla/CORSMode.h" +#include "nsIContentSecurityManager.h" +#include "nsIChannel.h" +#include "nsIChannelEventSink.h" +#include "nsILoadInfo.h" + +class nsILoadInfo; +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() = default; + + static nsresult doContentSecurityCheck( + nsIChannel* aChannel, nsCOMPtr<nsIStreamListener>& aInAndOutListener); + + static bool AllowTopLevelNavigationToDataURI(nsIChannel* aChannel); + static bool AllowInsecureRedirectToDataURI(nsIChannel* aNewChannel); + static void MeasureUnexpectedPrivilegedLoads(nsILoadInfo* aLoadInfo, + nsIURI* aFinalURI, + const nsACString& aRemoteType); + + enum CORSSecurityMapping { + // Disables all CORS checking overriding the value of aCORSMode. All checks + // are disabled even when CORSMode::CORS_ANONYMOUS or + // CORSMode::CORS_USE_CREDENTIALS is passed. This is mostly used for chrome + // code, where we don't need security checks. See + // SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL for the detailed explanation + // of the security mode. + DISABLE_CORS_CHECKS, + // Disables all CORS checking on CORSMode::CORS_NONE. The other two CORS + // modes CORSMode::CORS_ANONYMOUS and CORSMode::CORS_USE_CREDENTIALS are + // respected. + CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS, + // Allow load from any origin, but cross-origin requests require CORS. See + // SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT. Like above the other two + // CORS modes are unaffected and get parsed. + CORS_NONE_MAPS_TO_INHERITED_CONTEXT, + // Always require the server to acknowledge the request via CORS. + // CORSMode::CORS_NONE is parsed as if CORSMode::CORS_ANONYMOUS is passed. + REQUIRE_CORS_CHECKS, + }; + + // computes the security flags for the requested CORS mode + // @param aCORSSecurityMapping: See CORSSecurityMapping for variant + // descriptions + static nsSecurityFlags ComputeSecurityFlags( + mozilla::CORSMode aCORSMode, CORSSecurityMapping aCORSSecurityMapping); + + static void GetSerializedOrigin(nsIPrincipal* aOrigin, + nsIPrincipal* aResourceOrigin, + nsACString& aResult, nsILoadInfo* aLoadInfo); + + private: + static nsresult CheckChannel(nsIChannel* aChannel); + static nsresult CheckFTPSubresourceLoad(nsIChannel* aChannel); + static nsresult CheckAllowLoadInSystemPrivilegedContext(nsIChannel* aChannel); + static nsresult CheckAllowLoadInPrivilegedAboutContext(nsIChannel* aChannel); + static nsresult CheckChannelHasProtocolSecurityFlag(nsIChannel* aChannel); + static bool CrossOriginEmbedderPolicyAllowsCredentials(nsIChannel* aChannel); + + virtual ~nsContentSecurityManager() = default; +}; + +#endif /* nsContentSecurityManager_h___ */ diff --git a/dom/security/nsContentSecurityUtils.cpp b/dom/security/nsContentSecurityUtils.cpp new file mode 100644 index 0000000000..4b90f9bbab --- /dev/null +++ b/dom/security/nsContentSecurityUtils.cpp @@ -0,0 +1,1566 @@ +/* -*- 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/. */ + +/* A namespace class for static content security utilities. */ + +#include "nsContentSecurityUtils.h" + +#include "mozilla/Components.h" +#include "mozilla/dom/nsMixedContentBlocker.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "nsComponentManagerUtils.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIChannel.h" +#include "nsIHttpChannel.h" +#include "nsIMultiPartChannel.h" +#include "nsIURI.h" +#include "nsITransfer.h" +#include "nsNetUtil.h" +#include "nsSandboxFlags.h" +#if defined(XP_WIN) +# include "mozilla/WinHeaderOnlyUtils.h" +# include "WinUtils.h" +# include <wininet.h> +#endif + +#include "FramingChecker.h" +#include "js/Array.h" // JS::GetArrayLength +#include "js/ContextOptions.h" +#include "js/PropertyAndElement.h" // JS_GetElement +#include "js/RegExp.h" +#include "js/RegExpFlags.h" // JS::RegExpFlags +#include "js/friend/ErrorMessages.h" // JSMSG_UNSAFE_FILENAME +#include "mozilla/ExtensionPolicyService.h" +#include "mozilla/Logging.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/nsCSPContext.h" +#include "mozilla/StaticPrefs_security.h" +#include "LoadInfo.h" +#include "mozilla/StaticPrefs_extensions.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryComms.h" +#include "mozilla/TelemetryEventEnums.h" +#include "nsIConsoleService.h" +#include "nsIStringBundle.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::Telemetry; + +extern mozilla::LazyLogModule sCSMLog; +extern Atomic<bool, mozilla::Relaxed> sJSHacksChecked; +extern Atomic<bool, mozilla::Relaxed> sJSHacksPresent; +extern Atomic<bool, mozilla::Relaxed> sCSSHacksChecked; +extern Atomic<bool, mozilla::Relaxed> sCSSHacksPresent; +extern Atomic<bool, mozilla::Relaxed> sTelemetryEventEnabled; + +// Helper function for IsConsideredSameOriginForUIR which makes +// Principals of scheme 'http' return Principals of scheme 'https'. +static already_AddRefed<nsIPrincipal> MakeHTTPPrincipalHTTPS( + nsIPrincipal* aPrincipal) { + nsCOMPtr<nsIPrincipal> principal = aPrincipal; + // if the principal is not http, then it can also not be upgraded + // to https. + if (!principal->SchemeIs("http")) { + return principal.forget(); + } + + nsAutoCString spec; + aPrincipal->GetAsciiSpec(spec); + // replace http with https + spec.ReplaceLiteral(0, 4, "https"); + + nsCOMPtr<nsIURI> newURI; + nsresult rv = NS_NewURI(getter_AddRefs(newURI), spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + mozilla::OriginAttributes OA = + BasePrincipal::Cast(aPrincipal)->OriginAttributesRef(); + + principal = BasePrincipal::CreateContentPrincipal(newURI, OA); + return principal.forget(); +} + +/* static */ +bool nsContentSecurityUtils::IsConsideredSameOriginForUIR( + nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aResultPrincipal) { + MOZ_ASSERT(aTriggeringPrincipal); + MOZ_ASSERT(aResultPrincipal); + // we only have to make sure that the following truth table holds: + // aTriggeringPrincipal | aResultPrincipal | Result + // ---------------------------------------------------------------- + // http://example.com/foo.html | http://example.com/bar.html | true + // http://example.com/foo.html | https://example.com/bar.html | true + // https://example.com/foo.html | https://example.com/bar.html | true + // https://example.com/foo.html | http://example.com/bar.html | true + + // fast path if both principals are same-origin + if (aTriggeringPrincipal->Equals(aResultPrincipal)) { + return true; + } + + // in case a principal uses a scheme of 'http' then we just upgrade to + // 'https' and use the principal equals comparison operator to check + // for same-origin. + nsCOMPtr<nsIPrincipal> compareTriggeringPrincipal = + MakeHTTPPrincipalHTTPS(aTriggeringPrincipal); + + nsCOMPtr<nsIPrincipal> compareResultPrincipal = + MakeHTTPPrincipalHTTPS(aResultPrincipal); + + return compareTriggeringPrincipal->Equals(compareResultPrincipal); +} + +/* + * Performs a Regular Expression match, optionally returning the results. + * This function is not safe to use OMT. + * + * @param aPattern The regex pattern + * @param aString The string to compare against + * @param aOnlyMatch Whether we want match results or only a true/false for + * the match + * @param aMatchResult Out param for whether or not the pattern matched + * @param aRegexResults Out param for the matches of the regex, if requested + * @returns nsresult indicating correct function operation or error + */ +nsresult RegexEval(const nsAString& aPattern, const nsAString& aString, + bool aOnlyMatch, bool& aMatchResult, + nsTArray<nsString>* aRegexResults = nullptr) { + MOZ_ASSERT(NS_IsMainThread()); + aMatchResult = false; + + mozilla::dom::AutoJSAPI jsapi; + jsapi.Init(); + + JSContext* cx = jsapi.cx(); + mozilla::AutoDisableJSInterruptCallback disabler(cx); + + // We can use the junk scope here, because we're just using it for regexp + // evaluation, not actual script execution, and we disable statics so that the + // evaluation does not interact with the execution global. + JSAutoRealm ar(cx, xpc::PrivilegedJunkScope()); + + JS::Rooted<JSObject*> regexp( + cx, JS::NewUCRegExpObject(cx, aPattern.BeginReading(), aPattern.Length(), + JS::RegExpFlag::Unicode)); + if (!regexp) { + return NS_ERROR_ILLEGAL_VALUE; + } + + JS::Rooted<JS::Value> regexResult(cx, JS::NullValue()); + + size_t index = 0; + if (!JS::ExecuteRegExpNoStatics(cx, regexp, aString.BeginReading(), + aString.Length(), &index, aOnlyMatch, + ®exResult)) { + return NS_ERROR_FAILURE; + } + + if (regexResult.isNull()) { + // On no match, ExecuteRegExpNoStatics returns Null + return NS_OK; + } + if (aOnlyMatch) { + // On match, with aOnlyMatch = true, ExecuteRegExpNoStatics returns boolean + // true. + MOZ_ASSERT(regexResult.isBoolean() && regexResult.toBoolean()); + aMatchResult = true; + return NS_OK; + } + if (aRegexResults == nullptr) { + return NS_ERROR_INVALID_ARG; + } + + // Now we know we have a result, and we need to extract it so we can read it. + uint32_t length; + JS::Rooted<JSObject*> regexResultObj(cx, ®exResult.toObject()); + if (!JS::GetArrayLength(cx, regexResultObj, &length)) { + return NS_ERROR_NOT_AVAILABLE; + } + MOZ_LOG(sCSMLog, LogLevel::Verbose, ("Regex Matched %i strings", length)); + + for (uint32_t i = 0; i < length; i++) { + JS::Rooted<JS::Value> element(cx); + if (!JS_GetElement(cx, regexResultObj, i, &element)) { + return NS_ERROR_NO_CONTENT; + } + + nsAutoJSString value; + if (!value.init(cx, element)) { + return NS_ERROR_NO_CONTENT; + } + + MOZ_LOG(sCSMLog, LogLevel::Verbose, + ("Regex Matching: %i: %s", i, NS_ConvertUTF16toUTF8(value).get())); + aRegexResults->AppendElement(value); + } + + aMatchResult = true; + return NS_OK; +} + +/* + * MOZ_CRASH_UNSAFE_PRINTF has a sPrintfCrashReasonSize-sized buffer. We need + * to make sure we don't exceed it. These functions perform this check and + * munge things for us. + * + */ + +/* + * Destructively truncates a string to fit within the limit + */ +char* nsContentSecurityUtils::SmartFormatCrashString(const char* str) { + return nsContentSecurityUtils::SmartFormatCrashString(strdup(str)); +} + +char* nsContentSecurityUtils::SmartFormatCrashString(char* str) { + auto str_len = strlen(str); + + if (str_len > sPrintfCrashReasonSize) { + str[sPrintfCrashReasonSize - 1] = '\0'; + str_len = strlen(str); + } + MOZ_RELEASE_ASSERT(sPrintfCrashReasonSize > str_len); + + return str; +} + +/* + * Destructively truncates two strings to fit within the limit. + * format_string is a format string containing two %s entries + * The second string will be truncated to the _last_ 25 characters + * The first string will be truncated to the remaining limit. + */ +nsCString nsContentSecurityUtils::SmartFormatCrashString( + const char* part1, const char* part2, const char* format_string) { + return SmartFormatCrashString(strdup(part1), strdup(part2), format_string); +} + +nsCString nsContentSecurityUtils::SmartFormatCrashString( + char* part1, char* part2, const char* format_string) { + auto part1_len = strlen(part1); + auto part2_len = strlen(part2); + + auto constant_len = strlen(format_string) - 4; + + if (part1_len + part2_len + constant_len > sPrintfCrashReasonSize) { + if (part2_len > 25) { + part2 += (part2_len - 25); + } + part2_len = strlen(part2); + + part1[sPrintfCrashReasonSize - (constant_len + part2_len + 1)] = '\0'; + part1_len = strlen(part1); + } + MOZ_RELEASE_ASSERT(sPrintfCrashReasonSize > + constant_len + part1_len + part2_len); + + auto parts = nsPrintfCString(format_string, part1, part2); + return std::move(parts); +} + +/* + * Telemetry Events extra data only supports 80 characters, so we optimize the + * filename to be smaller and collect more data. + */ +nsString OptimizeFileName(const nsAString& aFileName) { + nsString optimizedName(aFileName); + + MOZ_LOG( + sCSMLog, LogLevel::Verbose, + ("Optimizing FileName: %s", NS_ConvertUTF16toUTF8(optimizedName).get())); + + optimizedName.ReplaceSubstring(u".xpi!"_ns, u"!"_ns); + optimizedName.ReplaceSubstring(u"shield.mozilla.org!"_ns, u"s!"_ns); + optimizedName.ReplaceSubstring(u"mozilla.org!"_ns, u"m!"_ns); + if (optimizedName.Length() > 80) { + optimizedName.Truncate(80); + } + + MOZ_LOG( + sCSMLog, LogLevel::Verbose, + ("Optimized FileName: %s", NS_ConvertUTF16toUTF8(optimizedName).get())); + return optimizedName; +} + +/* + * FilenameToFilenameType takes a fileName and returns a Pair of strings. + * The First entry is a string indicating the type of fileName + * The Second entry is a Maybe<string> that can contain additional details to + * report. + * + * The reason we use strings (instead of an int/enum) is because the Telemetry + * Events API only accepts strings. + * + * Function is a static member of the class to enable gtests. + */ + +/* static */ +FilenameTypeAndDetails nsContentSecurityUtils::FilenameToFilenameType( + const nsString& fileName, bool collectAdditionalExtensionData) { + // These are strings because the Telemetry Events API only accepts strings + static constexpr auto kChromeURI = "chromeuri"_ns; + static constexpr auto kResourceURI = "resourceuri"_ns; + static constexpr auto kBlobUri = "bloburi"_ns; + static constexpr auto kDataUri = "dataurl"_ns; + static constexpr auto kAboutUri = "abouturi"_ns; + static constexpr auto kDataUriWebExtCStyle = + "dataurl-extension-contentstyle"_ns; + static constexpr auto kSingleString = "singlestring"_ns; + static constexpr auto kMozillaExtensionFile = "mozillaextension_file"_ns; + static constexpr auto kOtherExtensionFile = "otherextension_file"_ns; + static constexpr auto kExtensionURI = "extension_uri"_ns; + static constexpr auto kSuspectedUserChromeJS = "suspectedUserChromeJS"_ns; +#if defined(XP_WIN) + static constexpr auto kSanitizedWindowsURL = "sanitizedWindowsURL"_ns; + static constexpr auto kSanitizedWindowsPath = "sanitizedWindowsPath"_ns; +#endif + static constexpr auto kOther = "other"_ns; + static constexpr auto kOtherWorker = "other-on-worker"_ns; + static constexpr auto kRegexFailure = "regexfailure"_ns; + + static constexpr auto kUCJSRegex = u"(.+).uc.js\\?*[0-9]*$"_ns; + static constexpr auto kExtensionRegex = u"extensions/(.+)@(.+)!(.+)$"_ns; + static constexpr auto kSingleFileRegex = u"^[a-zA-Z0-9.?]+$"_ns; + + if (fileName.IsEmpty()) { + return FilenameTypeAndDetails(kOther, Nothing()); + } + + // resource:// and chrome:// + if (StringBeginsWith(fileName, u"chrome://"_ns)) { + return FilenameTypeAndDetails(kChromeURI, Some(fileName)); + } + if (StringBeginsWith(fileName, u"resource://"_ns)) { + return FilenameTypeAndDetails(kResourceURI, Some(fileName)); + } + + // blob: and data: + if (StringBeginsWith(fileName, u"blob:"_ns)) { + return FilenameTypeAndDetails(kBlobUri, Nothing()); + } + if (StringBeginsWith(fileName, u"data:text/css;extension=style;"_ns)) { + return FilenameTypeAndDetails(kDataUriWebExtCStyle, Nothing()); + } + if (StringBeginsWith(fileName, u"data:"_ns)) { + return FilenameTypeAndDetails(kDataUri, Nothing()); + } + + // Can't do regex matching off-main-thread + if (NS_IsMainThread()) { + // Extension as loaded via a file:// + bool regexMatch; + nsTArray<nsString> regexResults; + nsresult rv = RegexEval(kExtensionRegex, fileName, /* aOnlyMatch = */ false, + regexMatch, ®exResults); + if (NS_FAILED(rv)) { + return FilenameTypeAndDetails(kRegexFailure, Nothing()); + } + if (regexMatch) { + nsCString type = StringEndsWith(regexResults[2], u"mozilla.org.xpi"_ns) + ? kMozillaExtensionFile + : kOtherExtensionFile; + const auto& extensionNameAndPath = + Substring(regexResults[0], ArrayLength("extensions/") - 1); + return FilenameTypeAndDetails( + type, Some(OptimizeFileName(extensionNameAndPath))); + } + + // Single File + rv = RegexEval(kSingleFileRegex, fileName, /* aOnlyMatch = */ true, + regexMatch); + if (NS_FAILED(rv)) { + return FilenameTypeAndDetails(kRegexFailure, Nothing()); + } + if (regexMatch) { + return FilenameTypeAndDetails(kSingleString, Some(fileName)); + } + + // Suspected userChromeJS script + rv = RegexEval(kUCJSRegex, fileName, /* aOnlyMatch = */ true, regexMatch); + if (NS_FAILED(rv)) { + return FilenameTypeAndDetails(kRegexFailure, Nothing()); + } + if (regexMatch) { + return FilenameTypeAndDetails(kSuspectedUserChromeJS, Nothing()); + } + } + + // Something loaded via an about:// URI. + if (StringBeginsWith(fileName, u"about:"_ns)) { + // Remove any querystrings and such + long int desired_length = fileName.Length(); + long int possible_new_length = 0; + + possible_new_length = fileName.FindChar('?'); + if (possible_new_length != -1 && possible_new_length < desired_length) { + desired_length = possible_new_length; + } + + possible_new_length = fileName.FindChar('#'); + if (possible_new_length != -1 && possible_new_length < desired_length) { + desired_length = possible_new_length; + } + + auto subFileName = Substring(fileName, 0, desired_length); + + return FilenameTypeAndDetails(kAboutUri, Some(subFileName)); + } + + // Something loaded via a moz-extension:// URI. + if (StringBeginsWith(fileName, u"moz-extension://"_ns)) { + if (!collectAdditionalExtensionData) { + return FilenameTypeAndDetails(kExtensionURI, Nothing()); + } + + nsAutoString sanitizedPathAndScheme; + sanitizedPathAndScheme.Append(u"moz-extension://["_ns); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), fileName); + if (NS_FAILED(rv)) { + // Return after adding ://[ so we know we failed here. + return FilenameTypeAndDetails(kExtensionURI, + Some(sanitizedPathAndScheme)); + } + + mozilla::extensions::URLInfo url(uri); + if (NS_IsMainThread()) { + // EPS is only usable on main thread + auto* policy = + ExtensionPolicyService::GetSingleton().GetByHost(url.Host()); + if (policy) { + nsString addOnId; + policy->GetId(addOnId); + + sanitizedPathAndScheme.Append(addOnId); + sanitizedPathAndScheme.Append(u": "_ns); + sanitizedPathAndScheme.Append(policy->Name()); + sanitizedPathAndScheme.Append(u"]"_ns); + + if (policy->IsPrivileged()) { + sanitizedPathAndScheme.Append(u"P=1"_ns); + } else { + sanitizedPathAndScheme.Append(u"P=0"_ns); + } + } else { + sanitizedPathAndScheme.Append(u"failed finding addon by host]"_ns); + } + } else { + sanitizedPathAndScheme.Append(u"can't get addon off main thread]"_ns); + } + + AppendUTF8toUTF16(url.FilePath(), sanitizedPathAndScheme); + return FilenameTypeAndDetails(kExtensionURI, Some(sanitizedPathAndScheme)); + } + +#if defined(XP_WIN) + auto flags = mozilla::widget::WinUtils::PathTransformFlags::Default | + mozilla::widget::WinUtils::PathTransformFlags::RequireFilePath; + nsAutoString strSanitizedPath(fileName); + if (widget::WinUtils::PreparePathForTelemetry(strSanitizedPath, flags)) { + DWORD cchDecodedUrl = INTERNET_MAX_URL_LENGTH; + WCHAR szOut[INTERNET_MAX_URL_LENGTH]; + HRESULT hr; + SAFECALL_URLMON_FUNC(CoInternetParseUrl, fileName.get(), PARSE_SCHEMA, 0, + szOut, INTERNET_MAX_URL_LENGTH, &cchDecodedUrl, 0); + if (hr == S_OK && cchDecodedUrl) { + nsAutoString sanitizedPathAndScheme; + sanitizedPathAndScheme.Append(szOut); + if (sanitizedPathAndScheme == u"file"_ns) { + sanitizedPathAndScheme.Append(u"://.../"_ns); + sanitizedPathAndScheme.Append(strSanitizedPath); + } + return FilenameTypeAndDetails(kSanitizedWindowsURL, + Some(sanitizedPathAndScheme)); + } else { + return FilenameTypeAndDetails(kSanitizedWindowsPath, + Some(strSanitizedPath)); + } + } +#endif + + if (!NS_IsMainThread()) { + return FilenameTypeAndDetails(kOtherWorker, Nothing()); + } + return FilenameTypeAndDetails(kOther, Nothing()); +} + +#ifdef NIGHTLY_BUILD +// Crash String must be safe from a telemetry point of view. +// This will be ensured when this function is used. +void PossiblyCrash(const char* aPrefSuffix, const char* aUnsafeCrashString, + const nsCString& aSafeCrashString) { + if (MOZ_UNLIKELY(!XRE_IsParentProcess())) { + // We only crash in the parent (unfortunately) because it's + // the only place we can be sure that our only-crash-once + // pref-writing works. + return; + } + if (!NS_IsMainThread()) { + // Setting a pref off the main thread causes ContentParent to observe the + // pref set, resulting in a Release Assertion when it tries to update the + // child off main thread. So don't do any of this off main thread. (Which + // is a bit of a blind spot for this purpose...) + return; + } + + nsCString previous_crashes("security.crash_tracking."); + previous_crashes.Append(aPrefSuffix); + previous_crashes.Append(".prevCrashes"); + + nsCString max_crashes("security.crash_tracking."); + max_crashes.Append(aPrefSuffix); + max_crashes.Append(".maxCrashes"); + + int32_t numberOfPreviousCrashes = 0; + numberOfPreviousCrashes = Preferences::GetInt(previous_crashes.get(), 0); + + int32_t maxAllowableCrashes = 0; + maxAllowableCrashes = Preferences::GetInt(max_crashes.get(), 0); + + if (numberOfPreviousCrashes >= maxAllowableCrashes) { + return; + } + + nsresult rv = + Preferences::SetInt(previous_crashes.get(), ++numberOfPreviousCrashes); + if (NS_FAILED(rv)) { + return; + } + + nsCOMPtr<nsIPrefService> prefsCom = Preferences::GetService(); + Preferences* prefs = static_cast<Preferences*>(prefsCom.get()); + + if (!prefs->AllowOffMainThreadSave()) { + // Do not crash if we can't save prefs off the main thread + return; + } + + rv = prefs->SavePrefFileBlocking(); + if (!NS_FAILED(rv)) { + // We can only use this in local builds where we don't send stuff up to the + // crash reporter because it has user private data. + // MOZ_CRASH_UNSAFE_PRINTF("%s", + // nsContentSecurityUtils::SmartFormatCrashString(aUnsafeCrashString)); + MOZ_CRASH_UNSAFE_PRINTF( + "%s", + nsContentSecurityUtils::SmartFormatCrashString(aSafeCrashString.get())); + } +} +#endif + +class EvalUsageNotificationRunnable final : public Runnable { + public: + EvalUsageNotificationRunnable(bool aIsSystemPrincipal, + NS_ConvertUTF8toUTF16& aFileNameA, + uint64_t aWindowID, uint32_t aLineNumber, + uint32_t aColumnNumber) + : mozilla::Runnable("EvalUsageNotificationRunnable"), + mIsSystemPrincipal(aIsSystemPrincipal), + mFileNameA(aFileNameA), + mWindowID(aWindowID), + mLineNumber(aLineNumber), + mColumnNumber(aColumnNumber) {} + + NS_IMETHOD Run() override { + nsContentSecurityUtils::NotifyEvalUsage( + mIsSystemPrincipal, mFileNameA, mWindowID, mLineNumber, mColumnNumber); + return NS_OK; + } + + void Revoke() {} + + private: + bool mIsSystemPrincipal; + NS_ConvertUTF8toUTF16 mFileNameA; + uint64_t mWindowID; + uint32_t mLineNumber; + uint32_t mColumnNumber; +}; + +/* static */ +bool nsContentSecurityUtils::IsEvalAllowed(JSContext* cx, + bool aIsSystemPrincipal, + const nsAString& aScript) { + // This allowlist contains files that are permanently allowed to use + // eval()-like functions. It will ideally be restricted to files that are + // exclusively used in testing contexts. + static nsLiteralCString evalAllowlist[] = { + // Test-only third-party library + "resource://testing-common/sinon-7.2.7.js"_ns, + // Test-only utility + "resource://testing-common/content-task.js"_ns, + + // Tracked by Bug 1584605 + "resource://gre/modules/translation/cld-worker.js"_ns, + + // require.js implements a script loader for workers. It uses eval + // to load the script; but injection is only possible in situations + // that you could otherwise control script that gets executed, so + // it is okay to allow eval() as it adds no additional attack surface. + // Bug 1584564 tracks requiring safe usage of require.js + "resource://gre/modules/workers/require.js"_ns, + + // The profiler's symbolication code uses a wasm module to extract symbols + // from the binary files result of local builds. + // See bug 1777479 + "resource://devtools/client/performance-new/symbolication.jsm.js"_ns, + + // The Browser Toolbox/Console + "debugger"_ns, + }; + + // We also permit two specific idioms in eval()-like contexts. We'd like to + // elminate these too; but there are in-the-wild Mozilla privileged extensions + // that use them. + static constexpr auto sAllowedEval1 = u"this"_ns; + static constexpr auto sAllowedEval2 = + u"function anonymous(\n) {\nreturn this\n}"_ns; + + if (MOZ_LIKELY(!aIsSystemPrincipal && !XRE_IsE10sParentProcess())) { + // We restrict eval in the system principal and parent process. + // Other uses (like web content and null principal) are allowed. + return true; + } + + if (JS::ContextOptionsRef(cx).disableEvalSecurityChecks()) { + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("Allowing eval() because this JSContext was set to allow it")); + return true; + } + + if (aIsSystemPrincipal && + StaticPrefs::security_allow_eval_with_system_principal()) { + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("Allowing eval() with System Principal because allowing pref is " + "enabled")); + return true; + } + + if (XRE_IsE10sParentProcess() && + StaticPrefs::security_allow_eval_in_parent_process()) { + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("Allowing eval() in parent process because allowing pref is " + "enabled")); + return true; + } + + DetectJsHacks(); + if (MOZ_UNLIKELY(sJSHacksPresent)) { + MOZ_LOG( + sCSMLog, LogLevel::Debug, + ("Allowing eval() %s because some " + "JS hacks may be present.", + (aIsSystemPrincipal ? "with System Principal" : "in parent process"))); + return true; + } + + if (XRE_IsE10sParentProcess() && + !StaticPrefs::extensions_webextensions_remote()) { + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("Allowing eval() in parent process because the web extension " + "process is disabled")); + return true; + } + + // We permit these two common idioms to get access to the global JS object + if (!aScript.IsEmpty() && + (aScript == sAllowedEval1 || aScript == sAllowedEval2)) { + MOZ_LOG( + sCSMLog, LogLevel::Debug, + ("Allowing eval() %s because a key string is " + "provided", + (aIsSystemPrincipal ? "with System Principal" : "in parent process"))); + return true; + } + + // Check the allowlist for the provided filename. getFilename is a helper + // function + nsAutoCString fileName; + uint32_t lineNumber = 0, columnNumber = 0; + nsJSUtils::GetCallingLocation(cx, fileName, &lineNumber, &columnNumber); + if (fileName.IsEmpty()) { + fileName = "unknown-file"_ns; + } + + NS_ConvertUTF8toUTF16 fileNameA(fileName); + for (const nsLiteralCString& allowlistEntry : evalAllowlist) { + // checking if current filename begins with entry, because JS Engine + // gives us additional stuff for code inside eval or Function ctor + // e.g., "require.js > Function" + if (StringBeginsWith(fileName, allowlistEntry)) { + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("Allowing eval() %s because the containing " + "file is in the allowlist", + (aIsSystemPrincipal ? "with System Principal" + : "in parent process"))); + return true; + } + } + + // Send Telemetry and Log to the Console + uint64_t windowID = nsJSUtils::GetCurrentlyRunningCodeInnerWindowID(cx); + if (NS_IsMainThread()) { + nsContentSecurityUtils::NotifyEvalUsage(aIsSystemPrincipal, fileNameA, + windowID, lineNumber, columnNumber); + } else { + auto runnable = new EvalUsageNotificationRunnable( + aIsSystemPrincipal, fileNameA, windowID, lineNumber, columnNumber); + NS_DispatchToMainThread(runnable); + } + + // Log to MOZ_LOG + MOZ_LOG(sCSMLog, LogLevel::Error, + ("Blocking eval() %s from file %s and script " + "provided %s", + (aIsSystemPrincipal ? "with System Principal" : "in parent process"), + fileName.get(), NS_ConvertUTF16toUTF8(aScript).get())); + + // Maybe Crash +#if defined(DEBUG) || defined(FUZZING) + auto crashString = nsContentSecurityUtils::SmartFormatCrashString( + NS_ConvertUTF16toUTF8(aScript).get(), fileName.get(), + (aIsSystemPrincipal + ? "Blocking eval() with System Principal with script %s from file %s" + : "Blocking eval() in parent process with script %s from file %s")); + MOZ_CRASH_UNSAFE_PRINTF("%s", crashString.get()); +#endif + + return false; +} + +/* static */ +void nsContentSecurityUtils::NotifyEvalUsage(bool aIsSystemPrincipal, + NS_ConvertUTF8toUTF16& aFileNameA, + uint64_t aWindowID, + uint32_t aLineNumber, + uint32_t aColumnNumber) { + // Send Telemetry + Telemetry::EventID eventType = + aIsSystemPrincipal ? Telemetry::EventID::Security_Evalusage_Systemcontext + : Telemetry::EventID::Security_Evalusage_Parentprocess; + + FilenameTypeAndDetails fileNameTypeAndDetails = + FilenameToFilenameType(aFileNameA, false); + mozilla::Maybe<nsTArray<EventExtraEntry>> extra; + if (fileNameTypeAndDetails.second.isSome()) { + extra = Some<nsTArray<EventExtraEntry>>({EventExtraEntry{ + "fileinfo"_ns, + NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value())}}); + } else { + extra = Nothing(); + } + if (!sTelemetryEventEnabled.exchange(true)) { + sTelemetryEventEnabled = true; + Telemetry::SetEventRecordingEnabled("security"_ns, true); + } + Telemetry::RecordEvent(eventType, mozilla::Some(fileNameTypeAndDetails.first), + extra); + + // Report an error to console + nsCOMPtr<nsIConsoleService> console( + do_GetService(NS_CONSOLESERVICE_CONTRACTID)); + if (!console) { + return; + } + nsCOMPtr<nsIScriptError> error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID)); + if (!error) { + return; + } + nsCOMPtr<nsIStringBundle> bundle; + nsCOMPtr<nsIStringBundleService> stringService = + mozilla::components::StringBundle::Service(); + if (!stringService) { + return; + } + stringService->CreateBundle( + "chrome://global/locale/security/security.properties", + getter_AddRefs(bundle)); + if (!bundle) { + return; + } + nsAutoString message; + AutoTArray<nsString, 1> formatStrings = {aFileNameA}; + nsresult rv = bundle->FormatStringFromName("RestrictBrowserEvalUsage", + formatStrings, message); + if (NS_FAILED(rv)) { + return; + } + + rv = error->InitWithWindowID(message, aFileNameA, u""_ns, aLineNumber, + aColumnNumber, nsIScriptError::errorFlag, + "BrowserEvalUsage", aWindowID, + true /* From chrome context */); + if (NS_FAILED(rv)) { + return; + } + console->LogMessage(error); +} + +/* static */ +void nsContentSecurityUtils::DetectJsHacks() { + // We can only perform the check of this preference on the Main Thread + // (because a String-based preference check is only safe on Main Thread.) + // In theory, it would be possible that a separate thread could get here + // before the main thread, resulting in the other thread not being able to + // perform this check, but the odds of that are small (and probably zero.) + if (!NS_IsMainThread()) { + return; + } + + // If the pref service isn't available, do nothing and re-do this later. + if (!Preferences::IsServiceAvailable()) { + return; + } + + // No need to check again. + if (MOZ_LIKELY(sJSHacksChecked || sJSHacksPresent)) { + return; + } + nsresult rv; + sJSHacksChecked = true; + + // This preference is required by bootstrapLoader.xpi, which is an + // alternate way to load legacy-style extensions. It only works on + // DevEdition/Nightly. + bool xpinstallSignatures; + rv = Preferences::GetBool("xpinstall.signatures.required", + &xpinstallSignatures, PrefValueKind::Default); + if (!NS_FAILED(rv) && !xpinstallSignatures) { + sJSHacksPresent = true; + return; + } + rv = Preferences::GetBool("xpinstall.signatures.required", + &xpinstallSignatures, PrefValueKind::User); + if (!NS_FAILED(rv) && !xpinstallSignatures) { + sJSHacksPresent = true; + return; + } + + // This preference is a file used for autoconfiguration of Firefox + // by administrators. It has also been (ab)used by the userChromeJS + // project to run legacy-style 'extensions', some of which use eval, + // all of which run in the System Principal context. + nsAutoString jsConfigPref; + rv = Preferences::GetString("general.config.filename", jsConfigPref, + PrefValueKind::Default); + if (!NS_FAILED(rv) && !jsConfigPref.IsEmpty()) { + sJSHacksPresent = true; + return; + } + rv = Preferences::GetString("general.config.filename", jsConfigPref, + PrefValueKind::User); + if (!NS_FAILED(rv) && !jsConfigPref.IsEmpty()) { + sJSHacksPresent = true; + return; + } + + // These preferences are for autoconfiguration of Firefox by admins. + // The first will load a file over the network; the second will + // fall back to a local file if the network is unavailable + nsAutoString configUrlPref; + rv = Preferences::GetString("autoadmin.global_config_url", configUrlPref, + PrefValueKind::Default); + if (!NS_FAILED(rv) && !configUrlPref.IsEmpty()) { + sJSHacksPresent = true; + return; + } + rv = Preferences::GetString("autoadmin.global_config_url", configUrlPref, + PrefValueKind::User); + if (!NS_FAILED(rv) && !configUrlPref.IsEmpty()) { + sJSHacksPresent = true; + return; + } + + bool failOverToCache; + rv = Preferences::GetBool("autoadmin.failover_to_cached", &failOverToCache, + PrefValueKind::Default); + if (!NS_FAILED(rv) && failOverToCache) { + sJSHacksPresent = true; + return; + } + rv = Preferences::GetBool("autoadmin.failover_to_cached", &failOverToCache, + PrefValueKind::User); + if (!NS_FAILED(rv) && failOverToCache) { + sJSHacksPresent = true; + } +} + +/* static */ +void nsContentSecurityUtils::DetectCssHacks() { + // We can only perform the check of this preference on the Main Thread + // It's possible that this function may therefore race and we expect the + // caller to ensure that the checks have actually happened. + if (!NS_IsMainThread()) { + return; + } + + // If the pref service isn't available, do nothing and re-do this later. + if (!Preferences::IsServiceAvailable()) { + return; + } + + // No need to check again. + if (MOZ_LIKELY(sCSSHacksChecked || sCSSHacksPresent)) { + return; + } + + // This preference is a bool to see if userChrome css is loaded + bool customStylesPresent = Preferences::GetBool( + "toolkit.legacyUserProfileCustomizations.stylesheets", false); + if (customStylesPresent) { + sCSSHacksPresent = true; + } + + sCSSHacksChecked = true; +} + +/* static */ +nsresult nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart( + nsIChannel* aChannel, nsIHttpChannel** aHttpChannel) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); + if (httpChannel) { + httpChannel.forget(aHttpChannel); + return NS_OK; + } + + nsCOMPtr<nsIMultiPartChannel> multipart = do_QueryInterface(aChannel); + if (!multipart) { + *aHttpChannel = nullptr; + return NS_OK; + } + + nsCOMPtr<nsIChannel> baseChannel; + nsresult rv = multipart->GetBaseChannel(getter_AddRefs(baseChannel)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + httpChannel = do_QueryInterface(baseChannel); + httpChannel.forget(aHttpChannel); + + return NS_OK; +} + +nsresult ParseCSPAndEnforceFrameAncestorCheck( + nsIChannel* aChannel, nsIContentSecurityPolicy** aOutCSP) { + MOZ_ASSERT(aChannel); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + ExtContentPolicyType contentType = loadInfo->GetExternalContentPolicyType(); + // frame-ancestor check only makes sense for subdocument and object loads, + // if this is not a load of such type, there is nothing to do here. + if (contentType != ExtContentPolicy::TYPE_SUBDOCUMENT && + contentType != ExtContentPolicy::TYPE_OBJECT) { + return NS_OK; + } + + // CSP can only hang off an http channel, if this channel is not + // an http channel then there is nothing to do here, + // except with add-ons, where the CSP is stored in a WebExtensionPolicy. + nsCOMPtr<nsIHttpChannel> httpChannel; + nsresult rv = nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart( + aChannel, getter_AddRefs(httpChannel)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString tCspHeaderValue, tCspROHeaderValue; + if (httpChannel) { + Unused << httpChannel->GetResponseHeader("content-security-policy"_ns, + tCspHeaderValue); + + Unused << httpChannel->GetResponseHeader( + "content-security-policy-report-only"_ns, tCspROHeaderValue); + + // if there are no CSP values, then there is nothing to do here. + if (tCspHeaderValue.IsEmpty() && tCspROHeaderValue.IsEmpty()) { + return NS_OK; + } + } + + nsCOMPtr<nsIPrincipal> resultPrincipal; + rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + aChannel, getter_AddRefs(resultPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<extensions::WebExtensionPolicy> addonPolicy; + if (!httpChannel) { + addonPolicy = BasePrincipal::Cast(resultPrincipal)->AddonPolicy(); + if (!addonPolicy) { + // Neither a HTTP channel, nor a moz-extension:-resource. + // CSP is not supported. + return NS_OK; + } + } + + RefPtr<nsCSPContext> csp = new nsCSPContext(); + // This CSPContext is only used for checking frame-ancestors, we + // will parse the CSP again anyway. (Unless this blocks the load, but + // parser warnings aren't really important in that case) + csp->SuppressParserLogMessages(); + + nsCOMPtr<nsIURI> selfURI; + nsAutoString referrerSpec; + if (httpChannel) { + aChannel->GetURI(getter_AddRefs(selfURI)); + nsCOMPtr<nsIReferrerInfo> referrerInfo = httpChannel->GetReferrerInfo(); + if (referrerInfo) { + referrerInfo->GetComputedReferrerSpec(referrerSpec); + } + } else { + // aChannel::GetURI would return the jar: or file:-URI for extensions. + // Use the "final" URI to get the actual moz-extension:-URL. + NS_GetFinalChannelURI(aChannel, getter_AddRefs(selfURI)); + } + + uint64_t innerWindowID = loadInfo->GetInnerWindowID(); + + rv = csp->SetRequestContextWithPrincipal(resultPrincipal, selfURI, + referrerSpec, innerWindowID); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (addonPolicy) { + csp->AppendPolicy(addonPolicy->BaseCSP(), false, false); + csp->AppendPolicy(addonPolicy->ExtensionPageCSP(), false, false); + } else { + NS_ConvertASCIItoUTF16 cspHeaderValue(tCspHeaderValue); + NS_ConvertASCIItoUTF16 cspROHeaderValue(tCspROHeaderValue); + + // ----- if there's a full-strength CSP header, apply it. + if (!cspHeaderValue.IsEmpty()) { + rv = CSP_AppendCSPFromHeader(csp, cspHeaderValue, false); + NS_ENSURE_SUCCESS(rv, rv); + } + + // ----- if there's a report-only CSP header, apply it. + if (!cspROHeaderValue.IsEmpty()) { + rv = CSP_AppendCSPFromHeader(csp, cspROHeaderValue, true); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // ----- Enforce frame-ancestor policy on any applied policies + bool safeAncestry = false; + // PermitsAncestry sends violation reports when necessary + rv = csp->PermitsAncestry(loadInfo, &safeAncestry); + + if (NS_FAILED(rv) || !safeAncestry) { + // stop! ERROR page! + aChannel->Cancel(NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION); + return NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION; + } + + // return the CSP for x-frame-options check + csp.forget(aOutCSP); + + return NS_OK; +} + +void EnforceXFrameOptionsCheck(nsIChannel* aChannel, + nsIContentSecurityPolicy* aCsp) { + MOZ_ASSERT(aChannel); + if (!FramingChecker::CheckFrameOptions(aChannel, aCsp)) { + // stop! ERROR page! + aChannel->Cancel(NS_ERROR_XFO_VIOLATION); + } +} + +/* static */ +void nsContentSecurityUtils::PerformCSPFrameAncestorAndXFOCheck( + nsIChannel* aChannel) { + nsCOMPtr<nsIContentSecurityPolicy> csp; + nsresult rv = + ParseCSPAndEnforceFrameAncestorCheck(aChannel, getter_AddRefs(csp)); + if (NS_FAILED(rv)) { + return; + } + + // X-Frame-Options needs to be enforced after CSP frame-ancestors + // checks because if frame-ancestors is present, then x-frame-options + // will be discarded + EnforceXFrameOptionsCheck(aChannel, csp); +} + +#if defined(DEBUG) +/* static */ +void nsContentSecurityUtils::AssertAboutPageHasCSP(Document* aDocument) { + // We want to get to a point where all about: pages ship with a CSP. This + // assertion ensures that we can not deploy new about: pages without a CSP. + // Please note that any about: page should not use inline JS or inline CSS, + // and instead should load JS and CSS from an external file (*.js, *.css) + // which allows us to apply a strong CSP omitting 'unsafe-inline'. Ideally, + // the CSP allows precisely the resources that need to be loaded; but it + // should at least be as strong as: + // <meta http-equiv="Content-Security-Policy" content="default-src chrome:; + // object-src 'none'"/> + + // This is a data document, created using DOMParser or + // document.implementation.createDocument() or such, not an about: page which + // is loaded as a web page. + if (aDocument->IsLoadedAsData()) { + return; + } + + // Check if we should skip the assertion + if (StaticPrefs::dom_security_skip_about_page_has_csp_assert()) { + return; + } + + // Check if we are loading an about: URI at all + nsCOMPtr<nsIURI> documentURI = aDocument->GetDocumentURI(); + if (!documentURI->SchemeIs("about")) { + return; + } + + nsCOMPtr<nsIContentSecurityPolicy> csp = aDocument->GetCsp(); + bool foundDefaultSrc = false; + bool foundObjectSrc = false; + bool foundUnsafeEval = false; + bool foundUnsafeInline = false; + bool foundScriptSrc = false; + bool foundWorkerSrc = false; + bool foundWebScheme = false; + if (csp) { + uint32_t policyCount = 0; + csp->GetPolicyCount(&policyCount); + nsAutoString parsedPolicyStr; + for (uint32_t i = 0; i < policyCount; ++i) { + csp->GetPolicyString(i, parsedPolicyStr); + if (parsedPolicyStr.Find(u"default-src") >= 0) { + foundDefaultSrc = true; + } + if (parsedPolicyStr.Find(u"object-src 'none'") >= 0) { + foundObjectSrc = true; + } + if (parsedPolicyStr.Find(u"'unsafe-eval'") >= 0) { + foundUnsafeEval = true; + } + if (parsedPolicyStr.Find(u"'unsafe-inline'") >= 0) { + foundUnsafeInline = true; + } + if (parsedPolicyStr.Find(u"script-src") >= 0) { + foundScriptSrc = true; + } + if (parsedPolicyStr.Find(u"worker-src") >= 0) { + foundWorkerSrc = true; + } + if (parsedPolicyStr.Find(u"http:") >= 0 || + parsedPolicyStr.Find(u"https:") >= 0) { + foundWebScheme = true; + } + } + } + + // Check if we should skip the allowlist and assert right away. Please note + // that this pref can and should only be set for automated testing. + if (StaticPrefs::dom_security_skip_about_page_csp_allowlist_and_assert()) { + NS_ASSERTION(foundDefaultSrc, "about: page must have a CSP"); + return; + } + + nsAutoCString aboutSpec; + documentURI->GetSpec(aboutSpec); + ToLowerCase(aboutSpec); + + // This allowlist contains about: pages that are permanently allowed to + // render without a CSP applied. + static nsLiteralCString sAllowedAboutPagesWithNoCSP[] = { + // about:blank is a special about page -> no CSP + "about:blank"_ns, + // about:srcdoc is a special about page -> no CSP + "about:srcdoc"_ns, + // about:sync-log displays plain text only -> no CSP + "about:sync-log"_ns, + // about:logo just displays the firefox logo -> no CSP + "about:logo"_ns, + // about:sync is a special mozilla-signed developer addon with low usage -> + // no CSP + "about:sync"_ns, +# if defined(ANDROID) + "about:config"_ns, +# endif + }; + + for (const nsLiteralCString& allowlistEntry : sAllowedAboutPagesWithNoCSP) { + // please note that we perform a substring match here on purpose, + // so we don't have to deal and parse out all the query arguments + // the various about pages rely on. + if (StringBeginsWith(aboutSpec, allowlistEntry)) { + return; + } + } + + MOZ_ASSERT(foundDefaultSrc, + "about: page must contain a CSP including default-src"); + MOZ_ASSERT(foundObjectSrc, + "about: page must contain a CSP denying object-src"); + + // preferences and downloads allow legacy inline scripts through hash src. + MOZ_ASSERT(!foundScriptSrc || + StringBeginsWith(aboutSpec, "about:preferences"_ns) || + StringBeginsWith(aboutSpec, "about:downloads"_ns) || + StringBeginsWith(aboutSpec, "about:newtab"_ns) || + StringBeginsWith(aboutSpec, "about:logins"_ns) || + StringBeginsWith(aboutSpec, "about:compat"_ns) || + StringBeginsWith(aboutSpec, "about:welcome"_ns) || + StringBeginsWith(aboutSpec, "about:profiling"_ns) || + StringBeginsWith(aboutSpec, "about:studies"_ns) || + StringBeginsWith(aboutSpec, "about:home"_ns), + "about: page must not contain a CSP including script-src"); + + MOZ_ASSERT(!foundWorkerSrc, + "about: page must not contain a CSP including worker-src"); + + // addons, preferences, debugging, ion, devtools all have to allow some + // remote web resources + MOZ_ASSERT(!foundWebScheme || + StringBeginsWith(aboutSpec, "about:preferences"_ns) || + StringBeginsWith(aboutSpec, "about:addons"_ns) || + StringBeginsWith(aboutSpec, "about:newtab"_ns) || + StringBeginsWith(aboutSpec, "about:debugging"_ns) || + StringBeginsWith(aboutSpec, "about:ion"_ns) || + StringBeginsWith(aboutSpec, "about:compat"_ns) || + StringBeginsWith(aboutSpec, "about:logins"_ns) || + StringBeginsWith(aboutSpec, "about:home"_ns) || + StringBeginsWith(aboutSpec, "about:welcome"_ns) || + StringBeginsWith(aboutSpec, "about:devtools"_ns) || + StringBeginsWith(aboutSpec, "about:pocket-saved"_ns) || + StringBeginsWith(aboutSpec, "about:pocket-home"_ns), + "about: page must not contain a CSP including a web scheme"); + + if (aDocument->IsExtensionPage()) { + // Extensions have two CSP policies applied where the baseline CSP + // includes 'unsafe-eval' and 'unsafe-inline', hence we have to skip + // the 'unsafe-eval' and 'unsafe-inline' assertions for extension + // pages. + return; + } + + MOZ_ASSERT(!foundUnsafeEval, + "about: page must not contain a CSP including 'unsafe-eval'"); + + static nsLiteralCString sLegacyUnsafeInlineAllowList[] = { + // Bug 1579160: Remove 'unsafe-inline' from style-src within + // about:preferences + "about:preferences"_ns, + // Bug 1571346: Remove 'unsafe-inline' from style-src within about:addons + "about:addons"_ns, + // Bug 1584485: Remove 'unsafe-inline' from style-src within: + // * about:newtab + // * about:welcome + // * about:home + "about:newtab"_ns, + "about:welcome"_ns, + "about:home"_ns, + }; + + for (const nsLiteralCString& aUnsafeInlineEntry : + sLegacyUnsafeInlineAllowList) { + // please note that we perform a substring match here on purpose, + // so we don't have to deal and parse out all the query arguments + // the various about pages rely on. + if (StringBeginsWith(aboutSpec, aUnsafeInlineEntry)) { + return; + } + } + + MOZ_ASSERT(!foundUnsafeInline, + "about: page must not contain a CSP including 'unsafe-inline'"); +} +#endif + +/* static */ +bool nsContentSecurityUtils::ValidateScriptFilename(JSContext* cx, + const char* aFilename) { + // If the pref is permissive, allow everything + if (StaticPrefs::security_allow_parent_unrestricted_js_loads()) { + return true; + } + + // If we're not in the parent process allow everything (presently) + if (!XRE_IsE10sParentProcess()) { + return true; + } + + // If we have allowed eval (because of a user configuration or more + // likely a test has requested it), and the script is an eval, allow it. + NS_ConvertUTF8toUTF16 filenameU(aFilename); + if (StaticPrefs::security_allow_eval_with_system_principal() || + StaticPrefs::security_allow_eval_in_parent_process()) { + if (StringEndsWith(filenameU, u"> eval"_ns)) { + return true; + } + } + + DetectJsHacks(); + + if (MOZ_UNLIKELY(!sJSHacksChecked)) { + MOZ_LOG( + sCSMLog, LogLevel::Debug, + ("Allowing a javascript load of %s because " + "we have not yet been able to determine if JS hacks may be present", + aFilename)); + return true; + } + + if (MOZ_UNLIKELY(sJSHacksPresent)) { + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("Allowing a javascript load of %s because " + "some JS hacks may be present", + aFilename)); + return true; + } + + if (XRE_IsE10sParentProcess() && + !StaticPrefs::extensions_webextensions_remote()) { + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("Allowing a javascript load of %s because the web extension " + "process is disabled.", + aFilename)); + return true; + } + + if (StringBeginsWith(filenameU, u"chrome://"_ns)) { + // If it's a chrome:// url, allow it + return true; + } + if (StringBeginsWith(filenameU, u"resource://"_ns)) { + // If it's a resource:// url, allow it + return true; + } + if (StringBeginsWith(filenameU, u"file://"_ns)) { + // We will temporarily allow all file:// URIs through for now + return true; + } + if (StringBeginsWith(filenameU, u"jar:file://"_ns)) { + // We will temporarily allow all jar URIs through for now + return true; + } + if (filenameU.Equals(u"about:sync-log"_ns)) { + // about:sync-log runs in the parent process and displays a directory + // listing. The listing has inline javascript that executes on load. + return true; + } + + if (StringBeginsWith(filenameU, u"moz-extension://"_ns)) { + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aFilename); + if (!NS_FAILED(rv) && NS_IsMainThread()) { + mozilla::extensions::URLInfo url(uri); + auto* policy = + ExtensionPolicyService::GetSingleton().GetByHost(url.Host()); + + if (policy && policy->IsPrivileged()) { + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("Allowing a javascript load of %s because the web extension " + "it is associated with is privileged.", + aFilename)); + return true; + } + } + } else if (!NS_IsMainThread()) { + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); + if (workerPrivate && workerPrivate->IsPrivilegedAddonGlobal()) { + MOZ_LOG(sCSMLog, LogLevel::Debug, + ("Allowing a javascript load of %s because the web extension " + "it is associated with is privileged.", + aFilename)); + return true; + } + } + + auto kAllowedFilenamesExact = { + // Allow through the injection provided by about:sync addon + u"data:,new function() {\n const { AboutSyncRedirector } = ChromeUtils.import(\"chrome://aboutsync/content/AboutSyncRedirector.js\");\n AboutSyncRedirector.register();\n}"_ns, + }; + + for (auto allowedFilename : kAllowedFilenamesExact) { + if (filenameU == allowedFilename) { + return true; + } + } + + auto kAllowedFilenamesPrefix = { + // Until 371900 is fixed, we need to do something about about:downloads + // and this is the most reasonable. See 1727770 + u"about:downloads"_ns, + // We think this is the same problem as about:downloads + u"about:preferences"_ns, + // Browser console will give a filename of 'debugger' See 1763943 + // Sometimes it's 'debugger eager eval code', other times just 'debugger + // eval code' + u"debugger"_ns}; + + for (auto allowedFilenamePrefix : kAllowedFilenamesPrefix) { + if (StringBeginsWith(filenameU, allowedFilenamePrefix)) { + return true; + } + } + + // Log to MOZ_LOG + MOZ_LOG(sCSMLog, LogLevel::Error, + ("ValidateScriptFilename Failed: %s\n", aFilename)); + + // Send Telemetry + FilenameTypeAndDetails fileNameTypeAndDetails = + FilenameToFilenameType(filenameU, true); + + Telemetry::EventID eventType = + Telemetry::EventID::Security_Javascriptload_Parentprocess; + + mozilla::Maybe<nsTArray<EventExtraEntry>> extra; + if (fileNameTypeAndDetails.second.isSome()) { + extra = Some<nsTArray<EventExtraEntry>>({EventExtraEntry{ + "fileinfo"_ns, + NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value())}}); + } else { + extra = Nothing(); + } + + if (!sTelemetryEventEnabled.exchange(true)) { + sTelemetryEventEnabled = true; + Telemetry::SetEventRecordingEnabled("security"_ns, true); + } + Telemetry::RecordEvent(eventType, mozilla::Some(fileNameTypeAndDetails.first), + extra); + +#if defined(DEBUG) || defined(FUZZING) + auto crashString = nsContentSecurityUtils::SmartFormatCrashString( + aFilename, + fileNameTypeAndDetails.second.isSome() + ? NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value()).get() + : "(None)", + "Blocking a script load %s from file %s"); + MOZ_CRASH_UNSAFE_PRINTF("%s", crashString.get()); +#elif defined(NIGHTLY_BUILD) + // Cause a crash (if we've never crashed before and we can ensure we won't do + // it again.) + // The details in the second arg, passed to UNSAFE_PRINTF, are also included + // in Event Telemetry and have received data review. + if (fileNameTypeAndDetails.second.isSome()) { + PossiblyCrash("js_load_1", aFilename, + NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value())); + } else { + PossiblyCrash("js_load_1", aFilename, "(None)"_ns); + } +#endif + + // Presently we are only enforcing restrictions for the script filename + // on Nightly. On all channels we are reporting Telemetry. In the future we + // will assert in debug builds and return false to prevent execution in + // non-debug builds. +#ifdef NIGHTLY_BUILD + return false; +#else + return true; +#endif +} + +/* static */ +void nsContentSecurityUtils::LogMessageToConsole(nsIHttpChannel* aChannel, + const char* aMsg) { + nsCOMPtr<nsIURI> uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) { + return; + } + + uint64_t windowID = 0; + rv = aChannel->GetTopLevelContentWindowId(&windowID); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + if (!windowID) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + loadInfo->GetInnerWindowID(&windowID); + } + + nsAutoString localizedMsg; + nsAutoCString spec; + uri->GetSpec(spec); + AutoTArray<nsString, 1> params = {NS_ConvertUTF8toUTF16(spec)}; + rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eSECURITY_PROPERTIES, aMsg, params, localizedMsg); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsContentUtils::ReportToConsoleByWindowID( + localizedMsg, nsIScriptError::warningFlag, "Security"_ns, windowID, uri); +} + +/* static */ +long nsContentSecurityUtils::ClassifyDownload( + nsIChannel* aChannel, const nsAutoCString& aMimeTypeGuess) { + MOZ_ASSERT(aChannel, "IsDownloadAllowed without channel?"); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + nsCOMPtr<nsIURI> contentLocation; + aChannel->GetURI(getter_AddRefs(contentLocation)); + + nsCOMPtr<nsIPrincipal> loadingPrincipal = loadInfo->GetLoadingPrincipal(); + if (!loadingPrincipal) { + loadingPrincipal = loadInfo->TriggeringPrincipal(); + } + // Creating a fake Loadinfo that is just used for the MCB check. + nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new mozilla::net::LoadInfo( + loadingPrincipal, loadInfo->TriggeringPrincipal(), nullptr, + nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, + nsIContentPolicy::TYPE_FETCH); + + int16_t decission = nsIContentPolicy::ACCEPT; + nsMixedContentBlocker::ShouldLoad(false, // aHadInsecureImageRedirect + contentLocation, // aContentLocation, + secCheckLoadInfo, // aLoadinfo + aMimeTypeGuess, // aMimeGuess, + false, // aReportError + &decission // aDecision + ); + Telemetry::Accumulate(mozilla::Telemetry::MIXED_CONTENT_DOWNLOADS, + decission != nsIContentPolicy::ACCEPT); + + if (StaticPrefs::dom_block_download_insecure() && + decission != nsIContentPolicy::ACCEPT) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); + if (httpChannel) { + LogMessageToConsole(httpChannel, "MixedContentBlockedDownload"); + } + return nsITransfer::DOWNLOAD_POTENTIALLY_UNSAFE; + } + + if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) { + return nsITransfer::DOWNLOAD_ACCEPTABLE; + } + + if (!StaticPrefs::dom_block_download_in_sandboxed_iframes()) { + return nsITransfer::DOWNLOAD_ACCEPTABLE; + } + + uint32_t triggeringFlags = loadInfo->GetTriggeringSandboxFlags(); + uint32_t currentflags = loadInfo->GetSandboxFlags(); + + if ((triggeringFlags & SANDBOXED_ALLOW_DOWNLOADS) || + (currentflags & SANDBOXED_ALLOW_DOWNLOADS)) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); + if (httpChannel) { + LogMessageToConsole(httpChannel, "IframeSandboxBlockedDownload"); + } + return nsITransfer::DOWNLOAD_FORBIDDEN; + } + + return nsITransfer::DOWNLOAD_ACCEPTABLE; +} diff --git a/dom/security/nsContentSecurityUtils.h b/dom/security/nsContentSecurityUtils.h new file mode 100644 index 0000000000..7cdf78d066 --- /dev/null +++ b/dom/security/nsContentSecurityUtils.h @@ -0,0 +1,87 @@ +/* -*- 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/. */ + +/* A namespace class for static content security utilities. */ + +#ifndef nsContentSecurityUtils_h___ +#define nsContentSecurityUtils_h___ + +#include <utility> +#include "mozilla/Maybe.h" +#include "nsStringFwd.h" + +struct JSContext; +class nsIChannel; +class nsIHttpChannel; +class nsIPrincipal; +class NS_ConvertUTF8toUTF16; + +namespace mozilla::dom { +class Document; +} // namespace mozilla::dom + +using FilenameTypeAndDetails = std::pair<nsCString, mozilla::Maybe<nsString>>; + +class nsContentSecurityUtils { + public: + // CSPs upgrade-insecure-requests directive applies to same origin top level + // navigations. Using the SOP would return false for the case when an https + // page triggers and http page to load, even though that http page would be + // upgraded to https later. Hence we have to use that custom function instead + // of simply calling aTriggeringPrincipal->Equals(aResultPrincipal). + static bool IsConsideredSameOriginForUIR(nsIPrincipal* aTriggeringPrincipal, + nsIPrincipal* aResultPrincipal); + + static bool IsEvalAllowed(JSContext* cx, bool aIsSystemPrincipal, + const nsAString& aScript); + static void NotifyEvalUsage(bool aIsSystemPrincipal, + NS_ConvertUTF8toUTF16& aFileNameA, + uint64_t aWindowID, uint32_t aLineNumber, + uint32_t aColumnNumber); + + // Helper function for various checks: + // This function detects profiles with userChrome.js or extension signatures + // disabled. We can't/won't enforce strong security for people with those + // hacks. The function will cache its result. + static void DetectJsHacks(); + // Helper function for detecting custom agent styles + static void DetectCssHacks(); + + // Helper function to query the HTTP Channel of a potential + // multi-part channel. Mostly used for querying response headers + static nsresult GetHttpChannelFromPotentialMultiPart( + nsIChannel* aChannel, nsIHttpChannel** aHttpChannel); + + // Helper function which performs the following framing checks + // * CSP frame-ancestors + // * x-frame-options + // If any of the two disallows framing, the channel will be cancelled. + static void PerformCSPFrameAncestorAndXFOCheck(nsIChannel* aChannel); + + // Helper function to Check if a Download is allowed; + static long ClassifyDownload(nsIChannel* aChannel, + const nsAutoCString& aMimeTypeGuess); + + // Public only for testing + static FilenameTypeAndDetails FilenameToFilenameType( + const nsString& fileName, bool collectAdditionalExtensionData); + static char* SmartFormatCrashString(const char* str); + static char* SmartFormatCrashString(char* str); + static nsCString SmartFormatCrashString(const char* part1, const char* part2, + const char* format_string); + static nsCString SmartFormatCrashString(char* part1, char* part2, + const char* format_string); + +#if defined(DEBUG) + static void AssertAboutPageHasCSP(mozilla::dom::Document* aDocument); +#endif + + static bool ValidateScriptFilename(JSContext* cx, const char* aFilename); + // Helper Function to Post a message to the corresponding JS-Console + static void LogMessageToConsole(nsIHttpChannel* aChannel, const char* aMsg); +}; + +#endif /* nsContentSecurityUtils_h___ */ diff --git a/dom/security/nsHTTPSOnlyStreamListener.cpp b/dom/security/nsHTTPSOnlyStreamListener.cpp new file mode 100644 index 0000000000..936cfd8cf6 --- /dev/null +++ b/dom/security/nsHTTPSOnlyStreamListener.cpp @@ -0,0 +1,275 @@ +/* -*- 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 "NSSErrorsService.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozpkix/pkixnss.h" +#include "nsCOMPtr.h" +#include "nsHTTPSOnlyStreamListener.h" +#include "nsHTTPSOnlyUtils.h" +#include "nsIChannel.h" +#include "nsIRequest.h" +#include "nsITransportSecurityInfo.h" +#include "nsIURI.h" +#include "nsPrintfCString.h" +#include "secerr.h" +#include "sslerr.h" + +NS_IMPL_ISUPPORTS(nsHTTPSOnlyStreamListener, nsIStreamListener, + nsIRequestObserver) + +nsHTTPSOnlyStreamListener::nsHTTPSOnlyStreamListener( + nsIStreamListener* aListener, nsILoadInfo* aLoadInfo) + : mListener(aListener), mCreationStart(mozilla::TimeStamp::Now()) { + RefPtr<mozilla::dom::WindowGlobalParent> wgp = + mozilla::dom::WindowGlobalParent::GetByInnerWindowId( + aLoadInfo->GetInnerWindowID()); + // For Top-level document loads (which don't have a requesting window-context) + // we compute these flags once we create the Document in nsSecureBrowserUI. + if (wgp) { + wgp->TopWindowContext()->AddSecurityState( + nsIWebProgressListener::STATE_HTTPS_ONLY_MODE_UPGRADED); + } +} + +NS_IMETHODIMP +nsHTTPSOnlyStreamListener::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + return mListener->OnDataAvailable(aRequest, aInputStream, aOffset, aCount); +} + +NS_IMETHODIMP +nsHTTPSOnlyStreamListener::OnStartRequest(nsIRequest* request) { + return mListener->OnStartRequest(request); +} + +NS_IMETHODIMP +nsHTTPSOnlyStreamListener::OnStopRequest(nsIRequest* request, + nsresult aStatus) { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request); + + // Note: CouldBeHttpsOnlyError also returns true if there was no error + if (nsHTTPSOnlyUtils::CouldBeHttpsOnlyError(channel, aStatus)) { + RecordUpgradeTelemetry(request, aStatus); + LogUpgradeFailure(request, aStatus); + + // If the request failed and there is a requesting window-context, set + // HTTPS-Only state flag to indicate a failed upgrade. + // For Top-level document loads (which don't have a requesting + // window-context) we simply check in the UI code whether we landed on the + // HTTPS-Only error page. + if (NS_FAILED(aStatus)) { + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + RefPtr<mozilla::dom::WindowGlobalParent> wgp = + mozilla::dom::WindowGlobalParent::GetByInnerWindowId( + loadInfo->GetInnerWindowID()); + + if (wgp) { + wgp->TopWindowContext()->AddSecurityState( + nsIWebProgressListener::STATE_HTTPS_ONLY_MODE_UPGRADE_FAILED); + } + } + } + + return mListener->OnStopRequest(request, aStatus); +} + +void nsHTTPSOnlyStreamListener::RecordUpgradeTelemetry(nsIRequest* request, + nsresult aStatus) { + // 1. Get time between now and when the initial upgrade request started + int64_t duration = + (mozilla::TimeStamp::Now() - mCreationStart).ToMilliseconds(); + + // 2. Assemble the category string + // [!] All strings have to be present in Histograms.json + nsresult rv; + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv); + if (NS_FAILED(rv)) { + return; + } + + nsAutoCString category; + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + nsContentPolicyType internalType = loadInfo->InternalContentPolicyType(); + + if (internalType == nsIContentPolicy::TYPE_DOCUMENT) { + category.AppendLiteral("top_"); + } else { + category.AppendLiteral("sub_"); + } + + if (NS_SUCCEEDED(aStatus)) { + category.AppendLiteral("successful"); + } else { + int32_t code = -1 * NS_ERROR_GET_CODE(aStatus); + + if (aStatus == NS_ERROR_REDIRECT_LOOP) { + category.AppendLiteral("f_redirectloop"); + } else if (aStatus == NS_ERROR_NET_TIMEOUT || + aStatus == NS_ERROR_NET_TIMEOUT_EXTERNAL) { + category.AppendLiteral("f_timeout"); + } else if (aStatus == NS_BINDING_ABORTED) { + category.AppendLiteral("f_aborted"); + } else if (aStatus == NS_ERROR_CONNECTION_REFUSED) { + category.AppendLiteral("f_cxnrefused"); + } else if (mozilla::psm::IsNSSErrorCode(code)) { + switch (code) { + case mozilla::pkix::MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT: + category.AppendLiteral("f_ssl_selfsignd"); + break; + case SSL_ERROR_BAD_CERT_DOMAIN: + category.AppendLiteral("f_ssl_badcertdm"); + break; + case SEC_ERROR_UNKNOWN_ISSUER: + category.AppendLiteral("f_ssl_unkwnissr"); + break; + default: + category.AppendLiteral("f_ssl_other"); + break; + } + } else { + category.AppendLiteral("f_other"); + } + } + mozilla::Telemetry::Accumulate( + mozilla::Telemetry::HTTPS_ONLY_MODE_UPGRADE_TIME_MS, category, duration); + + bool success = NS_SUCCEEDED(aStatus); + ExtContentPolicyType externalType = loadInfo->GetExternalContentPolicyType(); + auto typeKey = nsAutoCString("unknown"); + + if (externalType == ExtContentPolicy::TYPE_MEDIA) { + switch (internalType) { + case nsIContentPolicy::TYPE_INTERNAL_AUDIO: + case nsIContentPolicy::TYPE_INTERNAL_TRACK: + typeKey = "audio"_ns; + break; + + case nsIContentPolicy::TYPE_INTERNAL_VIDEO: + typeKey = "video"_ns; + break; + + default: + MOZ_ASSERT_UNREACHABLE(); + break; + } + } else { + switch (externalType) { + case ExtContentPolicy::TYPE_SCRIPT: + typeKey = "script"_ns; + break; + + case ExtContentPolicy::TYPE_OBJECT: + case ExtContentPolicy::TYPE_OBJECT_SUBREQUEST: + typeKey = "object"_ns; + break; + + case ExtContentPolicy::TYPE_DOCUMENT: + typeKey = "document"_ns; + break; + + case ExtContentPolicy::TYPE_SUBDOCUMENT: + typeKey = "subdocument"_ns; + break; + + case ExtContentPolicy::TYPE_XMLHTTPREQUEST: + typeKey = "xmlhttprequest"_ns; + break; + + case ExtContentPolicy::TYPE_IMAGE: + case ExtContentPolicy::TYPE_IMAGESET: + typeKey = "image"_ns; + break; + + case ExtContentPolicy::TYPE_DTD: + typeKey = "dtd"_ns; + break; + + case ExtContentPolicy::TYPE_FONT: + case ExtContentPolicy::TYPE_UA_FONT: + typeKey = "font"_ns; + break; + + case ExtContentPolicy::TYPE_FETCH: + typeKey = "fetch"_ns; + break; + + case ExtContentPolicy::TYPE_WEBSOCKET: + typeKey = "websocket"_ns; + break; + + case ExtContentPolicy::TYPE_STYLESHEET: + typeKey = "stylesheet"_ns; + break; + + case ExtContentPolicy::TYPE_CSP_REPORT: + typeKey = "cspreport"_ns; + break; + + case ExtContentPolicy::TYPE_WEB_MANIFEST: + typeKey = "webmanifest"_ns; + break; + + case ExtContentPolicy::TYPE_PING: + typeKey = "ping"_ns; + break; + + case ExtContentPolicy::TYPE_XSLT: + typeKey = "xslt"_ns; + break; + + case ExtContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA: + typeKey = "proxied-webrtc"_ns; + break; + + case ExtContentPolicy::TYPE_INVALID: + case ExtContentPolicy::TYPE_OTHER: + case ExtContentPolicy::TYPE_MEDIA: // already handled above + case ExtContentPolicy::TYPE_BEACON: + case ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD: + case ExtContentPolicy::TYPE_SPECULATIVE: + break; + // Do not add default: so that compilers can catch the missing case. + } + } + + mozilla::Telemetry::Accumulate( + mozilla::Telemetry::HTTPS_ONLY_MODE_UPGRADE_TYPE, typeKey, success); +} + +void nsHTTPSOnlyStreamListener::LogUpgradeFailure(nsIRequest* request, + nsresult aStatus) { + // If the request failed we'll log it to the console with the error-code + if (NS_SUCCEEDED(aStatus)) { + return; + } + nsresult rv; + // Try to query for the channel-object + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv); + if (NS_FAILED(rv)) { + return; + } + + nsCOMPtr<nsIURI> uri; + rv = channel->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) { + return; + } + // Logging URI as well as Module- and Error-Code + AutoTArray<nsString, 2> params = { + NS_ConvertUTF8toUTF16(uri->GetSpecOrDefault()), + NS_ConvertUTF8toUTF16(nsPrintfCString("M%u-C%u", + NS_ERROR_GET_MODULE(aStatus), + NS_ERROR_GET_CODE(aStatus)))}; + + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyFailedRequest", params, + nsIScriptError::errorFlag, loadInfo, + uri); +} diff --git a/dom/security/nsHTTPSOnlyStreamListener.h b/dom/security/nsHTTPSOnlyStreamListener.h new file mode 100644 index 0000000000..a2ab4711e3 --- /dev/null +++ b/dom/security/nsHTTPSOnlyStreamListener.h @@ -0,0 +1,50 @@ +/* -*- 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 nsHTTPSOnlyStreamListener_h___ +#define nsHTTPSOnlyStreamListener_h___ + +#include "mozilla/TimeStamp.h" +#include "nsCOMPtr.h" +#include "nsIStreamListener.h" + +class nsILoadInfo; + +/** + * This event listener gets registered for requests that have been upgraded + * using the HTTPS-only mode to log failed upgrades to the console. + */ +class nsHTTPSOnlyStreamListener : public nsIStreamListener { + public: + // nsISupports methods + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + explicit nsHTTPSOnlyStreamListener(nsIStreamListener* aListener, + nsILoadInfo* aLoadInfo); + + private: + virtual ~nsHTTPSOnlyStreamListener() = default; + + /** + * Records telemetry about the upgraded request. + * @param aStatus Request object + */ + void RecordUpgradeTelemetry(nsIRequest* request, nsresult aStatus); + + /** + * Logs information to the console if the request failed. + * @param request Request object + * @param aStatus Status of request + */ + void LogUpgradeFailure(nsIRequest* request, nsresult aStatus); + + nsCOMPtr<nsIStreamListener> mListener; + mozilla::TimeStamp mCreationStart; +}; + +#endif /* nsHTTPSOnlyStreamListener_h___ */ diff --git a/dom/security/nsHTTPSOnlyUtils.cpp b/dom/security/nsHTTPSOnlyUtils.cpp new file mode 100644 index 0000000000..8c99a223eb --- /dev/null +++ b/dom/security/nsHTTPSOnlyUtils.cpp @@ -0,0 +1,1042 @@ +/* -*- 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/Components.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/net/DNS.h" +#include "nsContentUtils.h" +#include "nsHTTPSOnlyUtils.h" +#include "nsIConsoleService.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIHttpsOnlyModePermission.h" +#include "nsIPermissionManager.h" +#include "nsIPrincipal.h" +#include "nsIRedirectHistoryEntry.h" +#include "nsIScriptError.h" +#include "nsIURIMutator.h" +#include "nsNetUtil.h" +#include "prnetdb.h" + +/* static */ +bool nsHTTPSOnlyUtils::IsHttpsOnlyModeEnabled(bool aFromPrivateWindow) { + // if the general pref is set to true, then we always return + if (mozilla::StaticPrefs::dom_security_https_only_mode()) { + return true; + } + + // otherwise we check if executing in private browsing mode and return true + // if the PBM pref for HTTPS-Only is set. + if (aFromPrivateWindow && + mozilla::StaticPrefs::dom_security_https_only_mode_pbm()) { + return true; + } + return false; +} + +/* static */ +bool nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled(bool aFromPrivateWindow) { + // if the general pref is set to true, then we always return + if (mozilla::StaticPrefs::dom_security_https_first()) { + return true; + } + + // otherwise we check if executing in private browsing mode and return true + // if the PBM pref for HTTPS-First is set. + if (aFromPrivateWindow && + mozilla::StaticPrefs::dom_security_https_first_pbm()) { + return true; + } + return false; +} + +/* static */ +void nsHTTPSOnlyUtils::PotentiallyFireHttpRequestToShortenTimout( + mozilla::net::DocumentLoadListener* aDocumentLoadListener) { + // only send http background request to counter timeouts if the + // pref allows us to do that. + if (!mozilla::StaticPrefs:: + dom_security_https_only_mode_send_http_background_request()) { + return; + } + + nsCOMPtr<nsIChannel> channel = aDocumentLoadListener->GetChannel(); + if (!channel) { + return; + } + + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + + // if neither HTTPS-Only nor HTTPS-First mode is enabled, then there is + // nothing to do here. + if (!IsHttpsOnlyModeEnabled(isPrivateWin) && + !IsHttpsFirstModeEnabled(isPrivateWin)) { + return; + } + + // if we are not dealing with a top-level load, then there is nothing to do + // here. + if (loadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) { + return; + } + + // if the load is exempt, then there is nothing to do here. + uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus(); + if (httpsOnlyStatus & nsILoadInfo::nsILoadInfo::HTTPS_ONLY_EXEMPT) { + return; + } + + // if it's not an http channel, then there is nothing to do here. + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel)); + if (!httpChannel) { + return; + } + + // if it's not a GET method, then there is nothing to do here either. + nsAutoCString method; + mozilla::Unused << httpChannel->GetRequestMethod(method); + if (!method.EqualsLiteral("GET")) { + return; + } + + // if it's already an https channel, then there is nothing to do here. + nsCOMPtr<nsIURI> channelURI; + channel->GetURI(getter_AddRefs(channelURI)); + if (!channelURI->SchemeIs("http")) { + return; + } + + // HTTPS-First only applies to standard ports but HTTPS-Only brute forces + // all http connections to be https and overrules HTTPS-First. In case + // HTTPS-First is enabled, but HTTPS-Only is not enabled, we might return + // early if attempting to send a background request to a non standard port. + if (IsHttpsFirstModeEnabled(isPrivateWin) && + !IsHttpsOnlyModeEnabled(isPrivateWin)) { + int32_t port = 0; + nsresult rv = channelURI->GetPort(&port); + int defaultPortforScheme = NS_GetDefaultPort("http"); + if (NS_SUCCEEDED(rv) && port != defaultPortforScheme && port != -1) { + return; + } + } + + // Check for general exceptions + if (OnionException(channelURI) || LoopbackOrLocalException(channelURI)) { + return; + } + + RefPtr<nsIRunnable> task = + new TestHTTPAnswerRunnable(channelURI, aDocumentLoadListener); + NS_DispatchToMainThread(task.forget()); +} + +/* static */ +bool nsHTTPSOnlyUtils::ShouldUpgradeRequest(nsIURI* aURI, + nsILoadInfo* aLoadInfo) { + // 1. Check if the HTTPS-Only Mode is even enabled, before we do anything else + bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + if (!IsHttpsOnlyModeEnabled(isPrivateWin)) { + return false; + } + + // 2. Check for general exceptions + if (OnionException(aURI) || LoopbackOrLocalException(aURI)) { + return false; + } + + // 3. Check if NoUpgrade-flag is set in LoadInfo + uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus(); + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) { + AutoTArray<nsString, 1> params = { + NS_ConvertUTF8toUTF16(aURI->GetSpecOrDefault())}; + nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyNoUpgradeException", params, + nsIScriptError::infoFlag, aLoadInfo, + aURI); + return false; + } + + // All subresources of an exempt triggering principal are also exempt + if (aLoadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) { + if (!aLoadInfo->TriggeringPrincipal()->IsSystemPrincipal() && + TestIfPrincipalIsExempt(aLoadInfo->TriggeringPrincipal())) { + return false; + } + } + + // We can upgrade the request - let's log it to the console + // Appending an 's' to the scheme for the logging. (http -> https) + nsAutoCString scheme; + aURI->GetScheme(scheme); + scheme.AppendLiteral("s"); + NS_ConvertUTF8toUTF16 reportSpec(aURI->GetSpecOrDefault()); + NS_ConvertUTF8toUTF16 reportScheme(scheme); + + bool isSpeculative = aLoadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_SPECULATIVE; + AutoTArray<nsString, 2> params = {reportSpec, reportScheme}; + nsHTTPSOnlyUtils::LogLocalizedString( + isSpeculative ? "HTTPSOnlyUpgradeSpeculativeConnection" + : "HTTPSOnlyUpgradeRequest", + params, nsIScriptError::warningFlag, aLoadInfo, aURI); + + // If the status was not determined before, we now indicate that the request + // will get upgraded, but no event-listener has been registered yet. + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UNINITIALIZED) { + httpsOnlyStatus ^= nsILoadInfo::HTTPS_ONLY_UNINITIALIZED; + httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_NOT_REGISTERED; + aLoadInfo->SetHttpsOnlyStatus(httpsOnlyStatus); + } + return true; +} + +/* static */ +bool nsHTTPSOnlyUtils::ShouldUpgradeWebSocket(nsIURI* aURI, + nsILoadInfo* aLoadInfo) { + // 1. Check if the HTTPS-Only Mode is even enabled, before we do anything else + bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + if (!IsHttpsOnlyModeEnabled(isPrivateWin)) { + return false; + } + + // 2. Check for general exceptions + if (OnionException(aURI) || LoopbackOrLocalException(aURI)) { + return false; + } + + // 3. Check if NoUpgrade-flag is set in LoadInfo + uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus(); + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) { + // Let's log to the console, that we didn't upgrade this request + AutoTArray<nsString, 1> params = { + NS_ConvertUTF8toUTF16(aURI->GetSpecOrDefault())}; + nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyNoUpgradeException", params, + nsIScriptError::infoFlag, aLoadInfo, + aURI); + return false; + } + + // All subresources of an exempt triggering principal are also exempt. + if (!aLoadInfo->TriggeringPrincipal()->IsSystemPrincipal() && + TestIfPrincipalIsExempt(aLoadInfo->TriggeringPrincipal())) { + return false; + } + + // We can upgrade the request - let's log it to the console + // Appending an 's' to the scheme for the logging. (ws -> wss) + nsAutoCString scheme; + aURI->GetScheme(scheme); + scheme.AppendLiteral("s"); + NS_ConvertUTF8toUTF16 reportSpec(aURI->GetSpecOrDefault()); + NS_ConvertUTF8toUTF16 reportScheme(scheme); + + AutoTArray<nsString, 2> params = {reportSpec, reportScheme}; + nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyUpgradeRequest", params, + nsIScriptError::warningFlag, aLoadInfo, + aURI); + return true; +} + +/* static */ +bool nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop( + nsIURI* aURI, nsILoadInfo* aLoadInfo, + const mozilla::EnumSet<UpgradeDowngradeEndlessLoopOptions>& aOptions) { + // 1. Check if the HTTPS-Only/HTTPS-First is even enabled, before doing + // anything else + bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + bool enforceForHTTPSOnlyMode = + IsHttpsOnlyModeEnabled(isPrivateWin) && + aOptions.contains( + UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSOnlyMode); + bool enforceForHTTPSFirstMode = + IsHttpsFirstModeEnabled(isPrivateWin) && + aOptions.contains( + UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSFirstMode); + bool enforceForHTTPSRR = + aOptions.contains(UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSRR); + if (!enforceForHTTPSOnlyMode && !enforceForHTTPSFirstMode && + !enforceForHTTPSRR) { + return false; + } + + // 2. Check if the upgrade downgrade pref even wants us to try to break the + // cycle. In the case that HTTPS RR is presented, we ignore this pref. + if (!mozilla::StaticPrefs:: + dom_security_https_only_mode_break_upgrade_downgrade_endless_loop() && + !enforceForHTTPSRR) { + return false; + } + + // 3. If it's not a top-level load, then there is nothing to do here either. + if (aLoadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) { + return false; + } + + // 4. If the load is exempt, then it's defintely not related to https-only + uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus(); + if ((httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) && + !enforceForHTTPSRR) { + return false; + } + + // 5. If the URI to be loaded is not http, then it's defnitely no endless + // loop caused by https-only. + if (!aURI->SchemeIs("http")) { + return false; + } + + nsAutoCString uriHost; + aURI->GetAsciiHost(uriHost); + + auto uriAndPrincipalComparator = [&](nsIPrincipal* aPrincipal) { + nsAutoCString principalHost; + aPrincipal->GetAsciiHost(principalHost); + bool checkPath = mozilla::StaticPrefs:: + dom_security_https_only_check_path_upgrade_downgrade_endless_loop(); + if (!checkPath) { + return uriHost.Equals(principalHost); + } + + nsAutoCString uriPath; + nsresult rv = aURI->GetFilePath(uriPath); + if (NS_FAILED(rv)) { + return false; + } + nsAutoCString principalPath; + aPrincipal->GetFilePath(principalPath); + return uriHost.Equals(principalHost) && uriPath.Equals(principalPath); + }; + + // 6. Check actual redirects. If the Principal that kicked off the + // load/redirect is not https, then it's definitely not a redirect cause by + // https-only. If the scheme of the principal however is https and the + // asciiHost of the URI to be loaded and the asciiHost of the Principal are + // identical, then we are dealing with an upgrade downgrade scenario and we + // have to break the cycle. + if (!aLoadInfo->RedirectChain().IsEmpty()) { + nsCOMPtr<nsIPrincipal> redirectPrincipal; + for (nsIRedirectHistoryEntry* entry : aLoadInfo->RedirectChain()) { + entry->GetPrincipal(getter_AddRefs(redirectPrincipal)); + if (redirectPrincipal && redirectPrincipal->SchemeIs("https") && + uriAndPrincipalComparator(redirectPrincipal)) { + return true; + } + } + } else { + // 6.1 We should only check if this load is triggered by a user gesture + // when the redirect chain is empty, since this information is only useful + // in our case here. When the redirect chain is not empty, this load is + // defnitely triggered by redirection, not a user gesture. + if (aLoadInfo->GetHasValidUserGestureActivation()) { + return false; + } + } + + // 7. Meta redirects and JS based redirects (win.location). If the security + // context that triggered the load is not https, then it's defnitely no + // endless loop caused by https-only. If the scheme is http however and the + // asciiHost of the URI to be loaded matches the asciiHost of the Principal, + // then we are dealing with an upgrade downgrade scenario and we have to break + // the cycle. + nsCOMPtr<nsIPrincipal> triggeringPrincipal = aLoadInfo->TriggeringPrincipal(); + if (!triggeringPrincipal->SchemeIs("https")) { + return false; + } + + return uriAndPrincipalComparator(triggeringPrincipal); +} + +/* static */ +bool nsHTTPSOnlyUtils::ShouldUpgradeHttpsFirstRequest(nsIURI* aURI, + nsILoadInfo* aLoadInfo) { + // 1. Check if HTTPS-First Mode is enabled + bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + if (!IsHttpsFirstModeEnabled(isPrivateWin)) { + return false; + } + + // 2. HTTPS-First only upgrades top-level loads (and speculative connections) + ExtContentPolicyType contentType = aLoadInfo->GetExternalContentPolicyType(); + if (contentType != ExtContentPolicy::TYPE_DOCUMENT && + contentType != ExtContentPolicy::TYPE_SPECULATIVE) { + return false; + } + + // 3. Check for general exceptions + if (OnionException(aURI) || LoopbackOrLocalException(aURI)) { + return false; + } + + // 4. Don't upgrade if upgraded previously or exempt from upgrades + uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus(); + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST || + httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) { + return false; + } + + // 5. HTTPS-First Mode only upgrades default ports - do not upgrade the + // request to https if port is specified and not the default port of 80. + MOZ_ASSERT(aURI->SchemeIs("http"), "how come the request is not 'http'?"); + int defaultPortforScheme = NS_GetDefaultPort("http"); + // If no port is specified, then the API returns -1 to indicate the default + // port. + int32_t port = 0; + nsresult rv = aURI->GetPort(&port); + NS_ENSURE_SUCCESS(rv, false); + if (port != defaultPortforScheme && port != -1) { + return false; + } + // 6. Do not upgrade form submissions (for now), revisit within + // Bug 1720500: Revisit upgrading form submissions. + if (aLoadInfo->GetIsFormSubmission()) { + return false; + } + + // https-first needs to account for breaking upgrade-downgrade endless + // loops at this point because this function is called before we + // check the redirect limit in HttpBaseChannel. If we encounter + // a same-origin server side downgrade from e.g https://example.com + // to http://example.com then we simply not annotating the loadinfo + // and returning false from within this function. Please note that + // the handling for https-only mode is different from https-first mode, + // because https-only mode results in an exception page in case + // we encounter and endless upgrade downgrade loop. + bool isUpgradeDowngradeEndlessLoop = IsUpgradeDowngradeEndlessLoop( + aURI, aLoadInfo, + {UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSFirstMode}); + if (isUpgradeDowngradeEndlessLoop) { + return false; + } + + // We can upgrade the request - let's log to the console and set the status + // so we know that we upgraded the request. + nsAutoCString scheme; + aURI->GetScheme(scheme); + scheme.AppendLiteral("s"); + NS_ConvertUTF8toUTF16 reportSpec(aURI->GetSpecOrDefault()); + NS_ConvertUTF8toUTF16 reportScheme(scheme); + + bool isSpeculative = contentType == ExtContentPolicy::TYPE_SPECULATIVE; + AutoTArray<nsString, 2> params = {reportSpec, reportScheme}; + nsHTTPSOnlyUtils::LogLocalizedString( + isSpeculative ? "HTTPSOnlyUpgradeSpeculativeConnection" + : "HTTPSOnlyUpgradeRequest", + params, nsIScriptError::warningFlag, aLoadInfo, aURI, true); + + // Set flag so we know that we upgraded the request + httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST; + aLoadInfo->SetHttpsOnlyStatus(httpsOnlyStatus); + return true; +} + +/* static */ +already_AddRefed<nsIURI> +nsHTTPSOnlyUtils::PotentiallyDowngradeHttpsFirstRequest(nsIChannel* aChannel, + nsresult aStatus) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus(); + // Only downgrade if we this request was upgraded using HTTPS-First Mode + if (!(httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST)) { + return nullptr; + } + // Once loading is in progress we set that flag so that timeout counter + // measures do not kick in. + loadInfo->SetHttpsOnlyStatus( + httpsOnlyStatus | nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS); + + nsresult status = aStatus; + // Since 4xx and 5xx errors return NS_OK instead of NS_ERROR_*, we need + // to check each NS_OK for those errors. + // Only downgrade an NS_OK status if it is an 4xx or 5xx error. + if (NS_SUCCEEDED(aStatus)) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); + // If no httpChannel exists we have nothing to do here. + if (!httpChannel) { + return nullptr; + } + uint32_t responseStatus = 0; + if (NS_FAILED(httpChannel->GetResponseStatus(&responseStatus))) { + return nullptr; + } + + // In case we found one 4xx or 5xx error we need to log it later on, + // for that reason we flip the nsresult 'status' from 'NS_OK' to the + // corresponding NS_ERROR_*. + // To do so we convert the response status to an nsresult error + // Every NS_OK that is NOT an 4xx or 5xx error code won't get downgraded. + if (responseStatus >= 400 && responseStatus < 600) { + // HttpProxyResponseToErrorCode() maps 400 and 404 on + // the same error as a 500 status which would lead to no downgrade + // later on. For that reason we explicit filter for 400 and 404 status + // codes to log them correctly and to downgrade them if possible. + switch (responseStatus) { + case 400: + status = NS_ERROR_PROXY_BAD_REQUEST; + break; + case 404: + status = NS_ERROR_PROXY_NOT_FOUND; + break; + default: + status = mozilla::net::HttpProxyResponseToErrorCode(responseStatus); + break; + } + } + if (NS_SUCCEEDED(status)) { + return nullptr; + } + } + + // We're only downgrading if it's possible that the error was + // caused by the upgrade. + if (HttpsUpgradeUnrelatedErrorCode(status)) { + return nullptr; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, nullptr); + + // Only downgrade if the current scheme is (a) https or (b) view-source:https + nsAutoCString spec; + if (uri->SchemeIs("https")) { + rv = uri->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, nullptr); + } else if (uri->SchemeIs("view-source")) { + nsCOMPtr<nsINestedURI> nestedURI = do_QueryInterface(uri); + if (!nestedURI) { + return nullptr; + } + nsCOMPtr<nsIURI> innerURI; + rv = nestedURI->GetInnerURI(getter_AddRefs(innerURI)); + NS_ENSURE_SUCCESS(rv, nullptr); + if (!innerURI || !innerURI->SchemeIs("https")) { + return nullptr; + } + nsAutoCString innerSpec; + rv = innerURI->GetSpec(innerSpec); + NS_ENSURE_SUCCESS(rv, nullptr); + + spec.Append("view-source:"); + spec.Append(innerSpec); + } else { + return nullptr; + } + + // Change the scheme to http + if (spec.Find("https://") < 0) { + MOZ_ASSERT(false, "how can we end up here not dealing with an https: URI?"); + return nullptr; + } + spec.ReplaceSubstring("https://", "http://"); + + nsCOMPtr<nsIURI> newURI; + rv = NS_NewURI(getter_AddRefs(newURI), spec); + NS_ENSURE_SUCCESS(rv, nullptr); + + // Log downgrade to console + NS_ConvertUTF8toUTF16 reportSpec(uri->GetSpecOrDefault()); + AutoTArray<nsString, 1> params = {reportSpec}; + nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyFailedDowngradeAgain", params, + nsIScriptError::warningFlag, loadInfo, + uri, true); + + return newURI.forget(); +} + +/* static */ +bool nsHTTPSOnlyUtils::CouldBeHttpsOnlyError(nsIChannel* aChannel, + nsresult aError) { + // If there is no failed channel, then there is nothing to do here. + if (!aChannel) { + return false; + } + + // If HTTPS-Only Mode is not enabled, then there is nothing to do here. + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + if (!IsHttpsOnlyModeEnabled(isPrivateWin)) { + return false; + } + + // If the load is exempt or did not get upgraded, + // then there is nothing to do here. + uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus(); + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT || + httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UNINITIALIZED) { + return false; + } + + // If it's one of those errors, then most likely it's not a HTTPS-Only error + // (This list of errors is largely drawn from nsDocShell::DisplayLoadError()) + return !HttpsUpgradeUnrelatedErrorCode(aError); +} + +/* static */ +bool nsHTTPSOnlyUtils::TestIfPrincipalIsExempt(nsIPrincipal* aPrincipal) { + static nsCOMPtr<nsIPermissionManager> sPermMgr; + if (!sPermMgr) { + sPermMgr = mozilla::components::PermissionManager::Service(); + mozilla::ClearOnShutdown(&sPermMgr); + } + NS_ENSURE_TRUE(sPermMgr, false); + + uint32_t perm; + nsresult rv = sPermMgr->TestExactPermissionFromPrincipal( + aPrincipal, "https-only-load-insecure"_ns, &perm); + NS_ENSURE_SUCCESS(rv, false); + + return perm == nsIHttpsOnlyModePermission::LOAD_INSECURE_ALLOW || + perm == nsIHttpsOnlyModePermission::LOAD_INSECURE_ALLOW_SESSION; +} + +/* static */ +void nsHTTPSOnlyUtils::TestSitePermissionAndPotentiallyAddExemption( + nsIChannel* aChannel) { + NS_ENSURE_TRUE_VOID(aChannel); + + // if https-only mode is not enabled, then there is nothing to do here. + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + if (!IsHttpsOnlyModeEnabled(isPrivateWin)) { + return; + } + + // if it's not a top-level load then there is nothing to here. + ExtContentPolicyType type = loadInfo->GetExternalContentPolicyType(); + if (type != ExtContentPolicy::TYPE_DOCUMENT) { + return; + } + + // it it's not an http channel, then there is nothing to do here. + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); + if (!httpChannel) { + return; + } + + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + aChannel, getter_AddRefs(principal)); + NS_ENSURE_SUCCESS_VOID(rv); + + // We explicitly add or also remove the exemption flag, because this + // function is also consulted after redirects. + uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus(); + if (TestIfPrincipalIsExempt(principal)) { + httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_EXEMPT; + } else { + httpsOnlyStatus &= ~nsILoadInfo::HTTPS_ONLY_EXEMPT; + } + loadInfo->SetHttpsOnlyStatus(httpsOnlyStatus); +} + +/* static */ +bool nsHTTPSOnlyUtils::IsSafeToAcceptCORSOrMixedContent( + nsILoadInfo* aLoadInfo) { + // Check if the request is exempt from upgrades + if ((aLoadInfo->GetHttpsOnlyStatus() & nsILoadInfo::HTTPS_ONLY_EXEMPT)) { + return false; + } + // Check if HTTPS-Only Mode is enabled for this request + bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + return nsHTTPSOnlyUtils::IsHttpsOnlyModeEnabled(isPrivateWin); +} + +/* static */ +bool nsHTTPSOnlyUtils::HttpsUpgradeUnrelatedErrorCode(nsresult aError) { + return NS_ERROR_UNKNOWN_PROTOCOL == aError || + NS_ERROR_FILE_NOT_FOUND == aError || + NS_ERROR_FILE_ACCESS_DENIED == aError || + NS_ERROR_UNKNOWN_HOST == aError || NS_ERROR_PHISHING_URI == aError || + NS_ERROR_MALWARE_URI == aError || NS_ERROR_UNWANTED_URI == aError || + NS_ERROR_HARMFUL_URI == aError || NS_ERROR_CONTENT_CRASHED == aError || + NS_ERROR_FRAME_CRASHED == aError; +} + +/* ------ Logging ------ */ + +/* static */ +void nsHTTPSOnlyUtils::LogLocalizedString(const char* aName, + const nsTArray<nsString>& aParams, + uint32_t aFlags, + nsILoadInfo* aLoadInfo, nsIURI* aURI, + bool aUseHttpsFirst) { + nsAutoString logMsg; + nsContentUtils::FormatLocalizedString(nsContentUtils::eSECURITY_PROPERTIES, + aName, aParams, logMsg); + LogMessage(logMsg, aFlags, aLoadInfo, aURI, aUseHttpsFirst); +} + +/* static */ +void nsHTTPSOnlyUtils::LogMessage(const nsAString& aMessage, uint32_t aFlags, + nsILoadInfo* aLoadInfo, nsIURI* aURI, + bool aUseHttpsFirst) { + // do not log to the console if the loadinfo says we should not! + uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus(); + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_DO_NOT_LOG_TO_CONSOLE) { + return; + } + + // Prepending HTTPS-Only to the outgoing console message + nsString message; + message.Append(aUseHttpsFirst ? u"HTTPS-First Mode: "_ns + : u"HTTPS-Only Mode: "_ns); + message.Append(aMessage); + + // Allow for easy distinction in devtools code. + auto category = aUseHttpsFirst ? "HTTPSFirst"_ns : "HTTPSOnly"_ns; + + uint64_t innerWindowId = aLoadInfo->GetInnerWindowID(); + if (innerWindowId > 0) { + // Send to content console + nsContentUtils::ReportToConsoleByWindowID(message, aFlags, category, + innerWindowId, aURI); + } else { + // Send to browser console + bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + nsContentUtils::LogSimpleConsoleError(message, category, isPrivateWin, + true /* from chrome context */, + aFlags); + } +} + +/* ------ Exceptions ------ */ + +/* static */ +bool nsHTTPSOnlyUtils::OnionException(nsIURI* aURI) { + // Onion-host exception can get disabled with a pref + if (mozilla::StaticPrefs::dom_security_https_only_mode_upgrade_onion()) { + return false; + } + nsAutoCString host; + aURI->GetHost(host); + return StringEndsWith(host, ".onion"_ns); +} + +/* static */ +bool nsHTTPSOnlyUtils::LoopbackOrLocalException(nsIURI* aURI) { + nsAutoCString asciiHost; + nsresult rv = aURI->GetAsciiHost(asciiHost); + NS_ENSURE_SUCCESS(rv, false); + + // Let's make a quick check if the host matches these loopback strings + // before we do anything else + if (asciiHost.EqualsLiteral("localhost") || asciiHost.EqualsLiteral("::1")) { + return true; + } + + mozilla::net::NetAddr addr; + if (NS_FAILED(addr.InitFromString(asciiHost))) { + return false; + } + // Loopback IPs are always exempt + if (addr.IsLoopbackAddr()) { + return true; + } + + // Local IP exception can get disabled with a pref + bool upgradeLocal = + mozilla::StaticPrefs::dom_security_https_only_mode_upgrade_local(); + return (!upgradeLocal && addr.IsIPAddrLocal()); +} + +/* static */ +bool nsHTTPSOnlyUtils::IsEqualURIExceptSchemeAndRef(nsIURI* aHTTPSSchemeURI, + nsIURI* aOtherURI, + nsILoadInfo* aLoadInfo) { + // 1. Check if one of parameters is null then webpage can't be loaded yet + // and no further inspections are needed + if (!aHTTPSSchemeURI || !aOtherURI || !aLoadInfo) { + return false; + } + + // 2. If the URI to be loaded is not http, then same origin will be detected + // already + if (!mozilla::net::SchemeIsHTTP(aOtherURI)) { + return false; + } + + // 3. Check if the HTTPS-Only Mode is even enabled, before we do anything else + bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + if (!IsHttpsOnlyModeEnabled(isPrivateWin) && + !IsHttpsFirstModeEnabled(isPrivateWin)) { + return false; + } + + // 4. If the load is exempt, then it's defintely not related to https-only + uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus(); + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) { + return false; + } + + // 5. Create a new target URI with 'https' instead of 'http' and compare it + // to the current URI + int32_t port = 0; + nsresult rv = aOtherURI->GetPort(&port); + NS_ENSURE_SUCCESS(rv, false); + // a port of -1 indicates the default port, hence we upgrade from port 80 to + // port 443 + // otherwise we keep the port. + if (port == -1) { + port = NS_GetDefaultPort("https"); + } + nsCOMPtr<nsIURI> newHTTPSchemeURI; + rv = NS_MutateURI(aOtherURI) + .SetScheme("https"_ns) + .SetPort(port) + .Finalize(newHTTPSchemeURI); + NS_ENSURE_SUCCESS(rv, false); + + bool uriEquals = false; + if (NS_FAILED( + aHTTPSSchemeURI->EqualsExceptRef(newHTTPSchemeURI, &uriEquals))) { + return false; + } + + return uriEquals; +} + +/*static*/ +void nsHTTPSOnlyUtils::PotentiallyClearExemptFlag(nsILoadInfo* aLoadInfo) { + // if neither HTTPS-Only nor HTTPS-First mode is enabled, then there is + // nothing to do here. + bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + if (!IsHttpsOnlyModeEnabled(isPrivateWin) && + !IsHttpsFirstModeEnabled(isPrivateWin)) { + return; + } + // if it is not a top-level load we have nothing to do here + if (aLoadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) { + return; + } + uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus(); + // if request is not exempt we have nothing do here + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) { + // clear exempt flag + httpsOnlyStatus ^= nsILoadInfo::HTTPS_ONLY_EXEMPT; + aLoadInfo->SetHttpsOnlyStatus(httpsOnlyStatus); + } +} +///////////////////////////////////////////////////////////////////// +// Implementation of TestHTTPAnswerRunnable + +NS_IMPL_ISUPPORTS_INHERITED(TestHTTPAnswerRunnable, mozilla::Runnable, + nsIStreamListener, nsIInterfaceRequestor, + nsITimerCallback) + +TestHTTPAnswerRunnable::TestHTTPAnswerRunnable( + nsIURI* aURI, mozilla::net::DocumentLoadListener* aDocumentLoadListener) + : mozilla::Runnable("TestHTTPAnswerRunnable"), + mURI(aURI), + mDocumentLoadListener(aDocumentLoadListener) {} + +/* static */ +bool TestHTTPAnswerRunnable::IsBackgroundRequestRedirected( + nsIHttpChannel* aChannel) { + // If there is no background request (aChannel), then there is nothing + // to do here. + if (!aChannel) { + return false; + } + // If the request was not redirected, then there is nothing to do here. + nsCOMPtr<nsILoadInfo> loadinfo = aChannel->LoadInfo(); + if (loadinfo->RedirectChain().IsEmpty()) { + return false; + } + + // If the final URI is not targeting an https scheme, then we definitely not + // dealing with a 'same-origin' redirect. + nsCOMPtr<nsIURI> finalURI; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI)); + NS_ENSURE_SUCCESS(rv, false); + if (!finalURI->SchemeIs("https")) { + return false; + } + + // If the background request was not http, then there is nothing to do here. + nsCOMPtr<nsIPrincipal> firstURIPrincipal; + loadinfo->RedirectChain()[0]->GetPrincipal(getter_AddRefs(firstURIPrincipal)); + if (!firstURIPrincipal || !firstURIPrincipal->SchemeIs("http")) { + return false; + } + + // By now we have verified that the inital background request was http and + // that the redirected scheme is https. We want to find the following case + // where the background channel redirects to the https version of the + // top-level request. + // --> background channel: http://example.com + // |--> redirects to: https://example.com + // Now we have to check that the hosts are 'same-origin'. + nsAutoCString redirectHost; + nsAutoCString finalHost; + firstURIPrincipal->GetAsciiHost(redirectHost); + finalURI->GetAsciiHost(finalHost); + return finalHost.Equals(redirectHost); +} + +NS_IMETHODIMP +TestHTTPAnswerRunnable::OnStartRequest(nsIRequest* aRequest) { + // If the request status is not OK, it means it encountered some + // kind of error in which case we do not want to do anything. + nsresult requestStatus; + aRequest->GetStatus(&requestStatus); + if (requestStatus != NS_OK) { + return NS_OK; + } + + // Check if the original top-level channel which https-only is trying + // to upgrade is already in progress or if the channel is an auth channel. + // If it is in progress or Auth is in progress, then all good, if not + // then let's cancel that channel so we can dispaly the exception page. + nsCOMPtr<nsIChannel> docChannel = mDocumentLoadListener->GetChannel(); + nsCOMPtr<nsIHttpChannel> httpsOnlyChannel = do_QueryInterface(docChannel); + if (httpsOnlyChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = httpsOnlyChannel->LoadInfo(); + uint32_t topLevelLoadInProgress = + loadInfo->GetHttpsOnlyStatus() & + nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS; + + nsCOMPtr<nsIHttpChannelInternal> httpChannelInternal = + do_QueryInterface(httpsOnlyChannel); + bool isAuthChannel = false; + mozilla::Unused << httpChannelInternal->GetIsAuthChannel(&isAuthChannel); + // some server configurations need a long time to respond to an https + // connection, but also redirect any http connection to the https version of + // it. If the top-level load has not started yet, but the http background + // request redirects to https, then do not show the error page, but keep + // waiting for the https response of the upgraded top-level request. + if (!topLevelLoadInProgress) { + nsCOMPtr<nsIHttpChannel> backgroundHttpChannel = + do_QueryInterface(aRequest); + topLevelLoadInProgress = + IsBackgroundRequestRedirected(backgroundHttpChannel); + } + if (!topLevelLoadInProgress && !isAuthChannel) { + // Only really cancel the original top-level channel if it's + // status is still NS_OK, otherwise it might have already + // encountered some other error and was cancelled. + nsresult httpsOnlyChannelStatus; + httpsOnlyChannel->GetStatus(&httpsOnlyChannelStatus); + if (httpsOnlyChannelStatus == NS_OK) { + httpsOnlyChannel->Cancel(NS_ERROR_NET_TIMEOUT_EXTERNAL); + } + } + } + + // Cancel this http request because it has reached the end of it's + // lifetime at this point. + aRequest->Cancel(NS_ERROR_ABORT); + return NS_ERROR_ABORT; +} + +NS_IMETHODIMP +TestHTTPAnswerRunnable::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aStream, + uint64_t aOffset, uint32_t aCount) { + // TestHTTPAnswerRunnable only cares about ::OnStartRequest which + // will also cancel the request, so we should in fact never even + // get here. + MOZ_ASSERT(false, "how come we get to ::OnDataAvailable"); + return NS_OK; +} + +NS_IMETHODIMP +TestHTTPAnswerRunnable::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + // TestHTTPAnswerRunnable only cares about ::OnStartRequest + return NS_OK; +} + +NS_IMETHODIMP +TestHTTPAnswerRunnable::GetInterface(const nsIID& aIID, void** aResult) { + return QueryInterface(aIID, aResult); +} + +NS_IMETHODIMP +TestHTTPAnswerRunnable::Run() { + // Wait N milliseconds to give the original https request a heads start + // before firing up this http request in the background. By default the + // timer is set to 3 seconds. If the https request has not received + // any signal from the server during that time, than it's almost + // certain the upgraded request will result in time out. + uint32_t background_timer_ms = mozilla::StaticPrefs:: + dom_security_https_only_fire_http_request_background_timer_ms(); + + return NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, + background_timer_ms, nsITimer::TYPE_ONE_SHOT); +} + +NS_IMETHODIMP +TestHTTPAnswerRunnable::Notify(nsITimer* aTimer) { + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + + // If the original channel has already started loading at this point + // then there is no need to do the dance. + nsCOMPtr<nsIChannel> origChannel = mDocumentLoadListener->GetChannel(); + nsCOMPtr<nsILoadInfo> origLoadInfo = origChannel->LoadInfo(); + uint32_t origHttpsOnlyStatus = origLoadInfo->GetHttpsOnlyStatus(); + uint32_t topLevelLoadInProgress = + origHttpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS; + uint32_t downloadInProgress = + origHttpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_DOWNLOAD_IN_PROGRESS; + if (topLevelLoadInProgress || downloadInProgress) { + return NS_OK; + } + + mozilla::OriginAttributes attrs = origLoadInfo->GetOriginAttributes(); + RefPtr<nsIPrincipal> nullPrincipal = mozilla::NullPrincipal::Create(attrs); + + uint32_t loadFlags = + nsIRequest::LOAD_ANONYMOUS | nsIRequest::INHIBIT_CACHING | + nsIRequest::INHIBIT_PERSISTENT_CACHING | nsIRequest::LOAD_BYPASS_CACHE | + nsIChannel::LOAD_BYPASS_SERVICE_WORKER; + + // No need to connect to the URI including the path because we only care about + // the round trip time if a server responds to an http request. + nsCOMPtr<nsIURI> backgroundChannelURI; + nsAutoCString prePathStr; + nsresult rv = mURI->GetPrePath(prePathStr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = NS_NewURI(getter_AddRefs(backgroundChannelURI), prePathStr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // we are using TYPE_OTHER because TYPE_DOCUMENT might have side effects + nsCOMPtr<nsIChannel> testHTTPChannel; + rv = NS_NewChannel(getter_AddRefs(testHTTPChannel), backgroundChannelURI, + nullPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER, nullptr, nullptr, nullptr, + nullptr, loadFlags); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // We have exempt that load from HTTPS-Only to avoid getting upgraded + // to https as well. Additonally let's not log that request to the console + // because it might confuse end users. + nsCOMPtr<nsILoadInfo> loadInfo = testHTTPChannel->LoadInfo(); + uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus(); + httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_EXEMPT | + nsILoadInfo::HTTPS_ONLY_DO_NOT_LOG_TO_CONSOLE; + loadInfo->SetHttpsOnlyStatus(httpsOnlyStatus); + + testHTTPChannel->SetNotificationCallbacks(this); + testHTTPChannel->AsyncOpen(this); + return NS_OK; +} diff --git a/dom/security/nsHTTPSOnlyUtils.h b/dom/security/nsHTTPSOnlyUtils.h new file mode 100644 index 0000000000..5cf94ae6a9 --- /dev/null +++ b/dom/security/nsHTTPSOnlyUtils.h @@ -0,0 +1,254 @@ +/* -*- 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 nsHTTPSOnlyUtils_h___ +#define nsHTTPSOnlyUtils_h___ + +#include "nsIScriptError.h" +#include "nsISupports.h" +#include "mozilla/net/DocumentLoadListener.h" + +class nsHTTPSOnlyUtils { + public: + /** + * Returns if HTTPS-Only Mode preference is enabled + * @param aFromPrivateWindow true if executing in private browsing mode + * @return true if HTTPS-Only Mode is enabled + */ + static bool IsHttpsOnlyModeEnabled(bool aFromPrivateWindow); + + /** + * Returns if HTTPS-First Mode preference is enabled + * @param aFromPrivateWindow true if executing in private browsing mode + * @return true if HTTPS-First Mode is enabled + */ + static bool IsHttpsFirstModeEnabled(bool aFromPrivateWindow); + + /** + * Potentially fires an http request for a top-level load (provided by + * aDocumentLoadListener) in the background to avoid long timeouts in case + * the upgraded https top-level load most likely will result in timeout. + * @param aDocumentLoadListener The Document listener associated with + * the original top-level channel. + */ + static void PotentiallyFireHttpRequestToShortenTimout( + mozilla::net::DocumentLoadListener* aDocumentLoadListener); + + /** + * Determines if a request should get upgraded because of the HTTPS-Only mode. + * If true, the httpsOnlyStatus flag in LoadInfo gets updated and a message is + * logged in the console. + * @param aURI nsIURI of request + * @param aLoadInfo nsILoadInfo of request + * @return true if request should get upgraded + */ + static bool ShouldUpgradeRequest(nsIURI* aURI, nsILoadInfo* aLoadInfo); + + /** + * Determines if a request should get upgraded because of the HTTPS-Only mode. + * If true, a message is logged in the console. + * @param aURI nsIURI of request + * @param aLoadInfo nsILoadInfo of request + * @return true if request should get upgraded + */ + static bool ShouldUpgradeWebSocket(nsIURI* aURI, nsILoadInfo* aLoadInfo); + + /** + * Determines if we might get stuck in an upgrade-downgrade-endless loop + * where https-only upgrades the request to https and the website downgrades + * the scheme to http again causing an endless upgrade downgrade loop. E.g. + * https-only upgrades to https and the website answers with a meta-refresh + * to downgrade to same-origin http version. Similarly this method breaks + * the endless cycle for JS based redirects and 302 based redirects. + * Note this function is also used when we got an HTTPS RR for the website. + * @param aURI nsIURI of request + * @param aLoadInfo nsILoadInfo of request + * @param aOptions an options object indicating if the function + * should be consulted for https-only or https-first mode or + * the case that an HTTPS RR is presented. + * @return true if an endless loop is detected + */ + enum class UpgradeDowngradeEndlessLoopOptions { + EnforceForHTTPSOnlyMode, + EnforceForHTTPSFirstMode, + EnforceForHTTPSRR, + }; + static bool IsUpgradeDowngradeEndlessLoop( + nsIURI* aURI, nsILoadInfo* aLoadInfo, + const mozilla::EnumSet<UpgradeDowngradeEndlessLoopOptions>& aOptions = + {}); + + /** + * Determines if a request should get upgraded because of the HTTPS-First + * mode. If true, the httpsOnlyStatus in LoadInfo gets updated and a message + * is logged in the console. + * @param aURI nsIURI of request + * @param aLoadInfo nsILoadInfo of request + * @return true if request should get upgraded + */ + static bool ShouldUpgradeHttpsFirstRequest(nsIURI* aURI, + nsILoadInfo* aLoadInfo); + + /** + * Determines if the request was previously upgraded with HTTPS-First, creates + * a downgraded URI and logs to console. + * @param aStatus Status code + * @param aChannel Failed channel + * @return URI with http-scheme or nullptr + */ + static already_AddRefed<nsIURI> PotentiallyDowngradeHttpsFirstRequest( + nsIChannel* aChannel, nsresult aStatus); + + /** + * Checks if the error code is on a block-list of codes that are probably + * not related to a HTTPS-Only Mode upgrade. + * @param aChannel The failed Channel. + * @param aError Error Code from Request + * @return false if error is not related to upgrade + */ + static bool CouldBeHttpsOnlyError(nsIChannel* aChannel, nsresult aError); + + /** + * Logs localized message to either content console or browser console + * @param aName Localization key + * @param aParams Localization parameters + * @param aFlags Logging Flag (see nsIScriptError) + * @param aLoadInfo The loadinfo of the request. + * @param [aURI] Optional: URI to log + * @param [aUseHttpsFirst] Optional: Log using HTTPS-First (vs. HTTPS-Only) + */ + static void LogLocalizedString(const char* aName, + const nsTArray<nsString>& aParams, + uint32_t aFlags, nsILoadInfo* aLoadInfo, + nsIURI* aURI = nullptr, + bool aUseHttpsFirst = false); + + /** + * Tests if the HTTPS-Only upgrade exception is set for a given principal. + * @param aPrincipal The principal for whom the exception should be checked + * @return True if exempt + */ + static bool TestIfPrincipalIsExempt(nsIPrincipal* aPrincipal); + + /** + * Tests if the HTTPS-Only Mode upgrade exception is set for channel result + * principal and sets or removes the httpsOnlyStatus-flag on the loadinfo + * accordingly. + * Note: This function only adds an exemption for loads of TYPE_DOCUMENT. + * @param aChannel The channel to be checked + */ + static void TestSitePermissionAndPotentiallyAddExemption( + nsIChannel* aChannel); + + /** + * Checks whether CORS or mixed content requests are safe because they'll get + * upgraded to HTTPS + * @param aLoadInfo nsILoadInfo of request + * @return true if it's safe to accept + */ + static bool IsSafeToAcceptCORSOrMixedContent(nsILoadInfo* aLoadInfo); + + /** + * Checks if two URIs are same origin modulo the difference that + * aHTTPSchemeURI uses an http scheme. + * @param aHTTPSSchemeURI nsIURI using scheme of https + * @param aOtherURI nsIURI using scheme of http + * @param aLoadInfo nsILoadInfo of the request + * @return true, if URIs are equal except scheme and ref + */ + static bool IsEqualURIExceptSchemeAndRef(nsIURI* aHTTPSSchemeURI, + nsIURI* aOtherURI, + nsILoadInfo* aLoadInfo); + + /** + * Checks a top-level load, if it is exempt by HTTPS-First/ Only + * clear exemption flag. + * @param aLoadInfo nsILoadInfo of the request + */ + static void PotentiallyClearExemptFlag(nsILoadInfo* aLoadInfo); + + private: + /** + * Checks if it can be ruled out that the error has something + * to do with an HTTPS upgrade. + * @param aError error code + * @return true if error is unrelated to the upgrade + */ + static bool HttpsUpgradeUnrelatedErrorCode(nsresult aError); + /** + * Logs localized message to either content console or browser console + * @param aMessage Message to log + * @param aFlags Logging Flag (see nsIScriptError) + * @param aLoadInfo The loadinfo of the request. + * @param [aURI] Optional: URI to log + * @param [aUseHttpsFirst] Optional: Log using HTTPS-First (vs. HTTPS-Only) + */ + static void LogMessage(const nsAString& aMessage, uint32_t aFlags, + nsILoadInfo* aLoadInfo, nsIURI* aURI = nullptr, + bool aUseHttpsFirst = false); + + /** + * Checks whether the URI ends with .onion + * @param aURI URI object + * @return true if the URI is an Onion URI + */ + static bool OnionException(nsIURI* aURI); + + /** + * Checks whether the URI is a loopback- or local-IP + * @param aURI URI object + * @return true if the URI is either loopback or local + */ + static bool LoopbackOrLocalException(nsIURI* aURI); +}; + +/** + * Helper class to perform an http request with a N milliseconds + * delay. If that http request is 'receiving data' before the + * upgraded https request, then it's a strong indicator that + * the https request will result in a timeout and hence we + * cancel the https request which will result in displaying + * the exception page. + */ +class TestHTTPAnswerRunnable final : public mozilla::Runnable, + public nsIStreamListener, + public nsIInterfaceRequestor, + public nsITimerCallback { + public: + // TestHTTPAnswerRunnable needs to implement all these channel related + // interfaces because otherwise our Necko code is not happy, but we + // really only care about ::OnStartRequest. + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSITIMERCALLBACK + + explicit TestHTTPAnswerRunnable( + nsIURI* aURI, mozilla::net::DocumentLoadListener* aDocumentLoadListener); + + protected: + ~TestHTTPAnswerRunnable() = default; + + private: + /** + * Checks whether the HTTP background request results in a redirect + * to the same upgraded top-level HTTPS URL + * @param aChannel a nsIHttpChannel object + * @return true if the backgroundchannel is redirected + */ + static bool IsBackgroundRequestRedirected(nsIHttpChannel* aChannel); + + RefPtr<nsIURI> mURI; + // We're keeping a reference to DocumentLoadListener instead of a specific + // channel, because the current top-level channel can change (for example + // through redirects) + RefPtr<mozilla::net::DocumentLoadListener> mDocumentLoadListener; + RefPtr<nsITimer> mTimer; +}; + +#endif /* nsHTTPSOnlyUtils_h___ */ diff --git a/dom/security/nsIHttpsOnlyModePermission.idl b/dom/security/nsIHttpsOnlyModePermission.idl new file mode 100644 index 0000000000..7eabdb6715 --- /dev/null +++ b/dom/security/nsIHttpsOnlyModePermission.idl @@ -0,0 +1,26 @@ +/* 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 "nsISupports.idl" +/** + * An interface to test for cookie permissions + */ +[scriptable, uuid(73f4f039-d6ff-41a7-9eb3-00db57b0b7f4)] +interface nsIHttpsOnlyModePermission : nsISupports +{ + /** + * nsIPermissionManager permission values + */ + const uint32_t LOAD_INSECURE_DEFAULT = 0; + const uint32_t LOAD_INSECURE_ALLOW = 1; + const uint32_t LOAD_INSECURE_BLOCK = 2; + + /** + * additional values which do not match + * nsIPermissionManager. Keep space available to allow nsIPermissionManager to + * add values without colliding. ACCESS_SESSION is not directly returned by + * any methods on this interface. + */ + const uint32_t LOAD_INSECURE_ALLOW_SESSION = 9; +}; diff --git a/dom/security/nsMixedContentBlocker.cpp b/dom/security/nsMixedContentBlocker.cpp new file mode 100644 index 0000000000..93a60de85a --- /dev/null +++ b/dom/security/nsMixedContentBlocker.cpp @@ -0,0 +1,1040 @@ +/* -*- 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 "nsDocShell.h" +#include "nsIWebProgressListener.h" +#include "nsContentUtils.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/dom/Document.h" +#include "nsIChannel.h" +#include "nsIParentChannel.h" +#include "mozilla/Preferences.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsIProtocolHandler.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsISecureBrowserUI.h" +#include "nsIWebNavigation.h" +#include "nsLoadGroup.h" +#include "nsIScriptError.h" +#include "nsIURI.h" +#include "nsIChannelEventSink.h" +#include "nsNetUtil.h" +#include "nsAsyncRedirectVerifyHelper.h" +#include "mozilla/LoadInfo.h" +#include "nsISiteSecurityService.h" +#include "prnetdb.h" +#include "nsQueryObject.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/Logging.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_fission.h" +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/DNS.h" +#include "mozilla/net/DocumentLoadListener.h" +#include "mozilla/net/DocumentChannel.h" + +#include "mozilla/dom/nsHTTPSOnlyUtils.h" + +using namespace mozilla; +using namespace mozilla::dom; + +static mozilla::LazyLogModule sMCBLog("MCBLog"); + +enum nsMixedContentBlockerMessageType { eBlocked = 0x00, eUserOverride = 0x01 }; + +// Allowlist of hostnames that should be considered secure contexts even when +// served over http:// or ws:// +nsCString* nsMixedContentBlocker::sSecurecontextAllowlist = nullptr; +bool nsMixedContentBlocker::sSecurecontextAllowlistCached = false; + +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 +}; + +nsMixedContentBlocker::~nsMixedContentBlocker() = default; + +NS_IMPL_ISUPPORTS(nsMixedContentBlocker, nsIContentPolicy, nsIChannelEventSink) + +static void LogMixedContentMessage( + MixedContentTypes aClassification, nsIURI* aContentLocation, + uint64_t aInnerWindowID, nsMixedContentBlockerMessageType aMessageType, + nsIURI* aRequestingLocation, + const nsACString& aOverruleMessageLookUpKeyWithThis = ""_ns) { + 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"); + } + } + + // if the callee explicitly wants to use a special message for this + // console report, then we allow to overrule the default with the + // explicitly provided one here. + if (!aOverruleMessageLookUpKeyWithThis.IsEmpty()) { + messageLookupKey = aOverruleMessageLookUpKeyWithThis; + } + + nsAutoString localizedMsg; + AutoTArray<nsString, 1> params; + CopyUTF8toUTF16(aContentLocation->GetSpecOrDefault(), + *params.AppendElement()); + nsContentUtils::FormatLocalizedString(nsContentUtils::eSECURITY_PROPERTIES, + messageLookupKey.get(), params, + localizedMsg); + + nsContentUtils::ReportToConsoleByWindowID(localizedMsg, severityFlag, + messageCategory, aInnerWindowID, + aRequestingLocation); +} + +/* 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) { + mozilla::net::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); + RefPtr<net::DocumentLoadListener> docListener = + do_QueryObject(is_ipc_channel); + if (is_ipc_channel && !docListener) { + return NS_OK; + } + + // Don't do these checks if we're switching from DocumentChannel + // to a real channel. In that case, we should already have done + // the checks in the parent process. AsyncOnChannelRedirect + // isn't called in the content process if we switch process, + // so checking here would just hide bugs in the process switch + // cases. + if (RefPtr<net::DocumentChannel> docChannel = do_QueryObject(aOldChannel)) { + 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 = aOldChannel->LoadInfo(); + nsCOMPtr<nsIPrincipal> requestingPrincipal = loadInfo->GetLoadingPrincipal(); + + // 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. + if (requestingPrincipal) { + // We check to see if the loadingPrincipal is systemPrincipal and return + // early if it is + if (requestingPrincipal->IsSystemPrincipal()) { + return NS_OK; + } + } + + int16_t decision = REJECT_REQUEST; + rv = ShouldLoad(newUri, loadInfo, + ""_ns, // aMimeGuess + &decision); + if (NS_FAILED(rv)) { + autoCallback.DontCallback(); + aOldChannel->Cancel(NS_ERROR_DOM_BAD_URI); + return NS_BINDING_FAILED; + } + + // If the channel is about to load mixed content, abort the channel + if (!NS_CP_ACCEPTED(decision)) { + autoCallback.DontCallback(); + aOldChannel->Cancel(NS_ERROR_DOM_BAD_URI); + 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(nsIURI* aContentLocation, + nsILoadInfo* aLoadInfo, + const nsACString& aMimeGuess, + 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 + aContentLocation, aLoadInfo, aMimeGuess, true, aDecision); + + if (*aDecision == nsIContentPolicy::REJECT_REQUEST) { + NS_SetRequestBlockingReason(aLoadInfo, + nsILoadInfo::BLOCKING_REASON_MIXED_BLOCKED); + } + + return rv; +} + +bool nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackHost( + const nsACString& aAsciiHost) { + if (mozilla::net::IsLoopbackHostname(aAsciiHost)) { + return true; + } + + using namespace mozilla::net; + NetAddr addr; + if (NS_FAILED(addr.InitFromString(aAsciiHost))) { + return false; + } + + // Step 4 of + // https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy says + // we should only consider [::1]/128 as a potentially trustworthy IPv6 + // address, whereas for IPv4 127.0.0.1/8 are considered as potentially + // trustworthy. + return addr.IsLoopBackAddressWithoutIPv6Mapping(); +} + +bool nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL(nsIURI* aURL) { + if (!aURL) { + return false; + } + nsAutoCString asciiHost; + nsresult rv = aURL->GetAsciiHost(asciiHost); + NS_ENSURE_SUCCESS(rv, false); + return IsPotentiallyTrustworthyLoopbackHost(asciiHost); +} + +/* Maybe we have a .onion URL. Treat it as trustworthy as well if + * `dom.securecontext.allowlist_onions` is `true`. + */ +bool nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(nsIURI* aURL) { + if (!StaticPrefs::dom_securecontext_allowlist_onions()) { + return false; + } + + nsAutoCString host; + nsresult rv = aURL->GetHost(host); + NS_ENSURE_SUCCESS(rv, false); + return StringEndsWith(host, ".onion"_ns); +} + +// static +void nsMixedContentBlocker::OnPrefChange(const char* aPref, void* aClosure) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aPref, "dom.securecontext.allowlist")); + Preferences::GetCString("dom.securecontext.allowlist", + *sSecurecontextAllowlist); +} + +// static +void nsMixedContentBlocker::GetSecureContextAllowList(nsACString& aList) { + MOZ_ASSERT(NS_IsMainThread()); + if (!sSecurecontextAllowlistCached) { + MOZ_ASSERT(!sSecurecontextAllowlist); + sSecurecontextAllowlistCached = true; + sSecurecontextAllowlist = new nsCString(); + Preferences::RegisterCallbackAndCall(OnPrefChange, + "dom.securecontext.allowlist"); + } + aList = *sSecurecontextAllowlist; +} + +// static +void nsMixedContentBlocker::Shutdown() { + if (sSecurecontextAllowlist) { + delete sSecurecontextAllowlist; + sSecurecontextAllowlist = nullptr; + } +} + +bool nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(nsIURI* aURI) { + // The following implements: + // https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy + + nsAutoCString scheme; + nsresult rv = aURI->GetScheme(scheme); + if (NS_FAILED(rv)) { + return false; + } + + // Blobs are expected to inherit their principal so we don't expect to have + // a content 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. + // We use nsIProtocolHandler flags to determine which protocols we consider a + // priori authenticated. + bool aPrioriAuthenticated = false; + if (NS_FAILED(NS_URIChainHasFlags( + aURI, nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY, + &aPrioriAuthenticated))) { + return false; + } + + if (aPrioriAuthenticated) { + return true; + } + + nsAutoCString host; + rv = aURI->GetHost(host); + if (NS_FAILED(rv)) { + return false; + } + + if (IsPotentiallyTrustworthyLoopbackURL(aURI)) { + return true; + } + + // If a host is not considered secure according to the default algorithm, then + // check to see if it has been allowlisted by the user. We only apply this + // allowlist 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")) { + return false; + } + + nsAutoCString allowlist; + GetSecureContextAllowList(allowlist); + for (const nsACString& allowedHost : + nsCCharSeparatedTokenizer(allowlist, ',').ToRange()) { + if (host.Equals(allowedHost)) { + return true; + } + } + + // Maybe we have a .onion URL. Treat it as trustworthy as well if + // `dom.securecontext.allowlist_onions` is `true`. + if (nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(aURI)) { + return true; + } + return false; +} + +/* + * Return the URI of the precusor principal or the URI of aPrincipal if there is + * no precursor URI. + */ +static already_AddRefed<nsIURI> GetPrincipalURIOrPrecursorPrincipalURI( + nsIPrincipal* aPrincipal) { + nsCOMPtr<nsIURI> precursorURI = nullptr; + if (aPrincipal->GetIsNullPrincipal()) { + nsCOMPtr<nsIPrincipal> precursorPrin = aPrincipal->GetPrecursorPrincipal(); + precursorURI = precursorPrin ? precursorPrin->GetURI() : nullptr; + } + + return precursorURI ? precursorURI.forget() : aPrincipal->GetURI(); +} + +/* Static version of ShouldLoad() that contains all the Mixed Content Blocker + * logic. Called from non-static ShouldLoad(). + */ +nsresult nsMixedContentBlocker::ShouldLoad(bool aHadInsecureImageRedirect, + nsIURI* aContentLocation, + nsILoadInfo* aLoadInfo, + const nsACString& aMimeGuess, + bool aReportError, + int16_t* aDecision) { + // Asserting that we are on the main thread here and hence do not have to lock + // and unlock security.mixed_content.block_active_content and + // security.mixed_content.block_display_content before reading/writing to + // them. + MOZ_ASSERT(NS_IsMainThread()); + + if (MOZ_UNLIKELY(MOZ_LOG_TEST(sMCBLog, LogLevel::Verbose))) { + nsAutoCString asciiUrl; + aContentLocation->GetAsciiSpec(asciiUrl); + MOZ_LOG(sMCBLog, LogLevel::Verbose, ("shouldLoad:")); + MOZ_LOG(sMCBLog, LogLevel::Verbose, + (" - contentLocation: %s", asciiUrl.get())); + } + + nsContentPolicyType internalContentType = + aLoadInfo->InternalContentPolicyType(); + nsCOMPtr<nsIPrincipal> loadingPrincipal = aLoadInfo->GetLoadingPrincipal(); + nsCOMPtr<nsIPrincipal> triggeringPrincipal = aLoadInfo->TriggeringPrincipal(); + + if (MOZ_UNLIKELY(MOZ_LOG_TEST(sMCBLog, LogLevel::Verbose))) { + MOZ_LOG(sMCBLog, LogLevel::Verbose, + (" - internalContentPolicyType: %s", + NS_CP_ContentTypeName(internalContentType))); + + if (loadingPrincipal != nullptr) { + nsAutoCString loadingPrincipalAsciiUrl; + loadingPrincipal->GetAsciiSpec(loadingPrincipalAsciiUrl); + MOZ_LOG(sMCBLog, LogLevel::Verbose, + (" - loadingPrincipal: %s", loadingPrincipalAsciiUrl.get())); + } else { + MOZ_LOG(sMCBLog, LogLevel::Verbose, (" - loadingPrincipal: (nullptr)")); + } + + nsAutoCString triggeringPrincipalAsciiUrl; + triggeringPrincipal->GetAsciiSpec(triggeringPrincipalAsciiUrl); + MOZ_LOG(sMCBLog, LogLevel::Verbose, + (" - triggeringPrincipal: %s", triggeringPrincipalAsciiUrl.get())); + } + + RefPtr<WindowContext> requestingWindow = + WindowContext::GetById(aLoadInfo->GetInnerWindowID()); + + bool isPreload = nsContentUtils::IsPreloadType(internalContentType); + + // 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 = + internalContentType == nsIContentPolicy::TYPE_INTERNAL_WORKER || + internalContentType == nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER || + internalContentType == nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER; + ExtContentPolicyType contentType = + nsContentUtils::InternalContentPolicyTypeToExternal(internalContentType); + + // 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. + // + // TYPE_PROXIED_WEBRTC_MEDIA: Ordinarily, webrtc uses low-level sockets for + // peer-to-peer media, which bypasses this code entirely. However, when a + // web proxy is being used, the TCP and TLS webrtc connections are routed + // through the web proxy (using HTTP CONNECT), which causes these connections + // to be checked. We just skip mixed content blocking in that case. + + switch (contentType) { + // The top-level document cannot be mixed content by definition + case ExtContentPolicy::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 ExtContentPolicy::TYPE_WEBSOCKET: + *aDecision = ACCEPT; + return NS_OK; + + // TYPE_SAVEAS_DOWNLOAD: Save-link-as feature is used to download a + // resource + // without involving a docShell. This kind of loading must be + // allowed, if not disabled in the preferences. + // Creating insecure connections for a save-as link download is + // acceptable. This download is completely disconnected from the docShell, + // but still using the same loading principal. + + case ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD: + *aDecision = ACCEPT; + return NS_OK; + break; + + // It does not make sense to subject webrtc media connections to mixed + // content blocking, since those connections are peer-to-peer and will + // therefore almost never match the origin. + case ExtContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA: + *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 ExtContentPolicy::TYPE_IMAGE: + case ExtContentPolicy::TYPE_MEDIA: + classification = eMixedDisplay; + break; + case ExtContentPolicy::TYPE_OBJECT_SUBREQUEST: + if (StaticPrefs::security_mixed_content_block_object_subrequest()) { + classification = eMixedScript; + } else { + 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 ExtContentPolicy::TYPE_BEACON: + case ExtContentPolicy::TYPE_CSP_REPORT: + case ExtContentPolicy::TYPE_DTD: + case ExtContentPolicy::TYPE_FETCH: + case ExtContentPolicy::TYPE_FONT: + case ExtContentPolicy::TYPE_UA_FONT: + case ExtContentPolicy::TYPE_IMAGESET: + case ExtContentPolicy::TYPE_OBJECT: + case ExtContentPolicy::TYPE_SCRIPT: + case ExtContentPolicy::TYPE_STYLESHEET: + case ExtContentPolicy::TYPE_SUBDOCUMENT: + case ExtContentPolicy::TYPE_PING: + case ExtContentPolicy::TYPE_WEB_MANIFEST: + case ExtContentPolicy::TYPE_XMLHTTPREQUEST: + case ExtContentPolicy::TYPE_XSLT: + case ExtContentPolicy::TYPE_OTHER: + case ExtContentPolicy::TYPE_SPECULATIVE: + break; + + case ExtContentPolicy::TYPE_INVALID: + MOZ_ASSERT(false, "Mixed content of unknown type"); + // Do not add default: so that compilers can catch the missing case. + } + + // 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; + MOZ_LOG(sMCBLog, LogLevel::Verbose, + (" -> decision: Request will be rejected because the innermost " + "URI could not be " + "retrieved")); + return NS_OK; + } + + // 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 && + URISafeToBeLoadedInSecureContext(innerContentLocation)) { + *aDecision = ACCEPT; + return NS_OK; + } + + /* + * Most likely aLoadingPrincipal reflects the security context of the owning + * document for this mixed content check. There are cases where that is not + * true, hence we have to we process requests in the following order: + * 1) If the load is triggered by the SystemPrincipal, we allow the load. + * Content scripts from addon code do provide aTriggeringPrincipal, which + * is an ExpandedPrincipal. If encountered, we allow the load. + * 2) If aLoadingPrincipal does not yield to a requestingLocation, then we + * fall back to querying the requestingLocation from aTriggeringPrincipal. + * 3) If we still end up not having a requestingLocation, we reject the load. + */ + + // 1) Check if the load was triggered by the system (SystemPrincipal) or + // a content script from addons code (ExpandedPrincipal) in which case the + // load is not subject to mixed content blocking. + if (triggeringPrincipal) { + if (triggeringPrincipal->IsSystemPrincipal()) { + *aDecision = ACCEPT; + return NS_OK; + } + nsCOMPtr<nsIExpandedPrincipal> expanded = + do_QueryInterface(triggeringPrincipal); + if (expanded) { + *aDecision = ACCEPT; + return NS_OK; + } + } + + // 2) If aLoadingPrincipal does not provide a requestingLocation, then + // we fall back to to querying the requestingLocation from + // aTriggeringPrincipal. + nsCOMPtr<nsIURI> requestingLocation; + auto* baseLoadingPrincipal = BasePrincipal::Cast(loadingPrincipal); + if (baseLoadingPrincipal) { + requestingLocation = + GetPrincipalURIOrPrecursorPrincipalURI(baseLoadingPrincipal); + } + if (!requestingLocation) { + auto* baseTriggeringPrincipal = BasePrincipal::Cast(triggeringPrincipal); + if (baseTriggeringPrincipal) { + requestingLocation = + GetPrincipalURIOrPrecursorPrincipalURI(baseTriggeringPrincipal); + } + } + + // 3) 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; + MOZ_LOG(sMCBLog, LogLevel::Verbose, + (" -> decision: Request will be rejected because no requesting " + "location could be " + "gathered.")); + return NS_OK; + } + + // Check the parent scheme. If it is not an HTTPS page then mixed content + // restrictions do not apply. + nsCOMPtr<nsIURI> innerRequestingLocation = + NS_GetInnermostURI(requestingLocation); + if (!innerRequestingLocation) { + NS_ERROR("Can't get innerURI from requestingLocation"); + *aDecision = REJECT_REQUEST; + MOZ_LOG(sMCBLog, LogLevel::Verbose, + (" -> decision: Request will be rejected because the innermost " + "URI of the " + "requesting location could be gathered.")); + return NS_OK; + } + + bool parentIsHttps = innerRequestingLocation->SchemeIs("https"); + if (!parentIsHttps) { + *aDecision = ACCEPT; + MOZ_LOG(sMCBLog, LogLevel::Verbose, + (" -> decision: Request will be allowed because the requesting " + "location is not using " + "HTTPS.")); + return 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 = innerContentLocation->SchemeIs("https"); + MOZ_ASSERT(!isHttpsScheme); +#endif + *aDecision = REJECT_REQUEST; + MOZ_LOG(sMCBLog, LogLevel::Verbose, + (" -> decision: Request will be rejected, trying to load a worker " + "from an insecure origin.")); + return NS_OK; + } + + bool isHttpScheme = innerContentLocation->SchemeIs("http"); + if (isHttpScheme && IsPotentiallyTrustworthyOrigin(innerContentLocation)) { + *aDecision = ACCEPT; + return NS_OK; + } + + // Check if https-only mode upgrades this later anyway + if (nsHTTPSOnlyUtils::IsSafeToAcceptCORSOrMixedContent(aLoadInfo)) { + *aDecision = ACCEPT; + 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. + + // Carve-out: if we're in the parent and we're loading media, e.g. through + // webbrowserpersist, don't reject it if we can't find a docshell. + if (XRE_IsParentProcess() && !requestingWindow && + (contentType == ExtContentPolicy::TYPE_IMAGE || + contentType == ExtContentPolicy::TYPE_MEDIA)) { + *aDecision = ACCEPT; + return NS_OK; + } + // Otherwise, we must have a window + NS_ENSURE_TRUE(requestingWindow, NS_OK); + + if (isHttpScheme && aLoadInfo->GetUpgradeInsecureRequests()) { + *aDecision = ACCEPT; + return NS_OK; + } + + // Allow http: mixed content if we are choosing to upgrade them when the + // pref "security.mixed_content.upgrade_display_content" is true. + // This behaves like GetUpgradeInsecureRequests above in that the channel will + // be upgraded to https before fetching any data from the netwerk. + bool isUpgradableDisplayType = + nsContentUtils::IsUpgradableDisplayType(contentType) && + StaticPrefs::security_mixed_content_upgrade_display_content(); + if (isHttpScheme && isUpgradableDisplayType) { + *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 (aLoadInfo->GetBlockAllMixedContent()) { + // log a message to the console before returning. + nsAutoCString spec; + nsresult rv = aContentLocation->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + AutoTArray<nsString, 1> params; + CopyUTF8toUTF16(spec, *params.AppendElement()); + + CSP_LogLocalizedStr("blockAllMixedContent", params, + u""_ns, // aSourceFile + u""_ns, // aScriptSample + 0, // aLineNumber + 0, // aColumnNumber + nsIScriptError::errorFlag, "blockAllMixedContent"_ns, + requestingWindow->Id(), + !!aLoadInfo->GetOriginAttributes().mPrivateBrowsingId); + *aDecision = REJECT_REQUEST; + MOZ_LOG( + sMCBLog, LogLevel::Verbose, + (" -> decision: Request will be rejected because the CSP directive " + "'block-all-mixed-content' was set while trying to load data from " + "a non-secure origin.")); + return NS_OK; + } + + // Determine if the rootDoc is https and if the user decided to allow Mixed + // Content + WindowContext* topWC = requestingWindow->TopWindowContext(); + bool rootHasSecureConnection = topWC->GetIsSecure(); + bool allowMixedContent = topWC->GetAllowMixedContent(); + + // 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 (contentType == ExtContentPolicyType::TYPE_SUBDOCUMENT && + !rootHasSecureConnection) { + bool httpsParentExists = false; + + RefPtr<WindowContext> curWindow = requestingWindow; + while (!httpsParentExists && curWindow) { + httpsParentExists = curWindow->GetIsSecure(); + curWindow = curWindow->GetParentWindowContext(); + } + + if (!httpsParentExists) { + *aDecision = nsIContentPolicy::ACCEPT; + return NS_OK; + } + } + + OriginAttributes originAttributes; + if (loadingPrincipal) { + originAttributes = loadingPrincipal->OriginAttributesRef(); + } else if (triggeringPrincipal) { + originAttributes = triggeringPrincipal->OriginAttributesRef(); + } + + // 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, + originAttributes); + } else { + // Ask the parent process to do the same call + mozilla::dom::ContentChild* cc = + mozilla::dom::ContentChild::GetSingleton(); + if (cc) { + cc->SendAccumulateMixedContentHSTS(innerContentLocation, active, + originAttributes); + } + } + } + + // set hasMixedContentObjectSubrequest on this object if necessary + if (contentType == ExtContentPolicyType::TYPE_OBJECT_SUBREQUEST && + aReportError) { + if (!StaticPrefs::security_mixed_content_block_object_subrequest()) { + nsAutoCString messageLookUpKey( + "LoadingMixedDisplayObjectSubrequestDeprecation"); + + LogMixedContentMessage(classification, aContentLocation, topWC->Id(), + eUserOverride, requestingLocation, + messageLookUpKey); + } + } + + uint32_t newState = 0; + // If the content is display content, and the pref says display content should + // be blocked, block it. + if (classification == eMixedDisplay) { + if (!StaticPrefs::security_mixed_content_block_display_content() || + allowMixedContent) { + *aDecision = nsIContentPolicy::ACCEPT; + // User has overriden the pref and the root is not https; + // mixed display content was allowed on an https subframe. + newState |= nsIWebProgressListener::STATE_LOADED_MIXED_DISPLAY_CONTENT; + } else { + *aDecision = nsIContentPolicy::REJECT_REQUEST; + MOZ_LOG(sMCBLog, LogLevel::Verbose, + (" -> decision: Request will be rejected because the content is " + "display " + "content (blocked by pref " + "security.mixed_content.block_display_content).")); + newState |= nsIWebProgressListener::STATE_BLOCKED_MIXED_DISPLAY_CONTENT; + } + } else { + MOZ_ASSERT(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 (!StaticPrefs::security_mixed_content_block_active_content() || + allowMixedContent) { + *aDecision = nsIContentPolicy::ACCEPT; + // User has already overriden the pref and the root is not https; + // mixed active content was allowed on an https subframe. + newState |= nsIWebProgressListener::STATE_LOADED_MIXED_ACTIVE_CONTENT; + } else { + // User has not overriden the pref by Disabling protection. Reject the + // request and update the security state. + *aDecision = nsIContentPolicy::REJECT_REQUEST; + MOZ_LOG(sMCBLog, LogLevel::Verbose, + (" -> decision: Request will be rejected because the content is " + "active " + "content (blocked by pref " + "security.mixed_content.block_active_content).")); + // The user has not overriden the pref, so make sure they still have an + // option by calling nativeDocShell which will invoke the doorhanger + newState |= nsIWebProgressListener::STATE_BLOCKED_MIXED_ACTIVE_CONTENT; + } + } + + // To avoid duplicate errors on the console, we do not report blocked + // preloads to the console. + if (!isPreload && aReportError) { + LogMixedContentMessage(classification, aContentLocation, topWC->Id(), + (*aDecision == nsIContentPolicy::REJECT_REQUEST) + ? eBlocked + : eUserOverride, + requestingLocation); + } + + // Notify the top WindowContext of the flags we've computed, and it + // will handle updating any relevant security UI. + topWC->AddSecurityState(newState); + return NS_OK; +} + +bool nsMixedContentBlocker::URISafeToBeLoadedInSecureContext(nsIURI* aURI) { + /* Returns a bool if the URI can be loaded as a sub resource safely. + * + * 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_IS_POTENTIALLY_TRUSTWORTHY - e.g. + * "https", + * "moz-safe-about" + * + */ + bool schemeLocal = false; + bool schemeNoReturnData = false; + bool schemeInherits = false; + bool schemeSecure = false; + if (NS_FAILED(NS_URIChainHasFlags( + aURI, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE, &schemeLocal)) || + NS_FAILED(NS_URIChainHasFlags( + aURI, nsIProtocolHandler::URI_DOES_NOT_RETURN_DATA, + &schemeNoReturnData)) || + NS_FAILED(NS_URIChainHasFlags( + aURI, nsIProtocolHandler::URI_INHERITS_SECURITY_CONTEXT, + &schemeInherits)) || + NS_FAILED(NS_URIChainHasFlags( + aURI, nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY, + &schemeSecure))) { + return false; + } + + MOZ_LOG(sMCBLog, LogLevel::Verbose, + (" - URISafeToBeLoadedInSecureContext:")); + MOZ_LOG(sMCBLog, LogLevel::Verbose, (" - schemeLocal: %i", schemeLocal)); + MOZ_LOG(sMCBLog, LogLevel::Verbose, + (" - schemeNoReturnData: %i", schemeNoReturnData)); + MOZ_LOG(sMCBLog, LogLevel::Verbose, + (" - schemeInherits: %i", schemeInherits)); + MOZ_LOG(sMCBLog, LogLevel::Verbose, (" - schemeSecure: %i", schemeSecure)); + return (schemeLocal || schemeNoReturnData || schemeInherits || schemeSecure); +} + +NS_IMETHODIMP +nsMixedContentBlocker::ShouldProcess(nsIURI* aContentLocation, + nsILoadInfo* aLoadInfo, + const nsACString& aMimeGuess, + int16_t* aDecision) { + if (!aContentLocation) { + // aContentLocation may be null when a plugin is loading without an + // associated URI resource + if (aLoadInfo->GetExternalContentPolicyType() == + ExtContentPolicyType::TYPE_OBJECT) { + *aDecision = ACCEPT; + return NS_OK; + } + + NS_SetRequestBlockingReason(aLoadInfo, + nsILoadInfo::BLOCKING_REASON_MIXED_BLOCKED); + *aDecision = REJECT_REQUEST; + return NS_ERROR_FAILURE; + } + + return ShouldLoad(aContentLocation, aLoadInfo, aMimeGuess, aDecision); +} + +// 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, const OriginAttributes& aOriginAttributes) { + // 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(aURI, aOriginAttributes, &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); + } else { + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS, + MCB_HSTS_PASSIVE_WITH_HSTS); + } + } else { + if (!hsts) { + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS, + MCB_HSTS_ACTIVE_NO_HSTS); + } else { + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS, + MCB_HSTS_ACTIVE_WITH_HSTS); + } + } +} diff --git a/dom/security/nsMixedContentBlocker.h b/dom/security/nsMixedContentBlocker.h new file mode 100644 index 0000000000..abf409c8f1 --- /dev/null +++ b/dom/security/nsMixedContentBlocker.h @@ -0,0 +1,90 @@ +/* -*- 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" + +using mozilla::OriginAttributes; + +class nsILoadInfo; // forward declaration +namespace mozilla::net { +class nsProtocolProxyService; // forward declaration +} // namespace mozilla::net + +class nsMixedContentBlocker : public nsIContentPolicy, + public nsIChannelEventSink { + private: + virtual ~nsMixedContentBlocker(); + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTPOLICY + NS_DECL_NSICHANNELEVENTSINK + + nsMixedContentBlocker() = default; + + // See: + // https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy + static bool IsPotentiallyTrustworthyLoopbackHost( + const nsACString& aAsciiHost); + static bool IsPotentiallyTrustworthyLoopbackURL(nsIURI* aURL); + static bool IsPotentiallyTrustworthyOnion(nsIURI* aURL); + static bool IsPotentiallyTrustworthyOrigin(nsIURI* aURI); + + /* 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. + * @param aReportError + * boolean flag indicating if a rejection should automaticly be + * logged into the Console. + * Remaining parameters are from nsIContentPolicy::ShouldLoad(). + */ + static nsresult ShouldLoad(bool aHadInsecureImageRedirect, + nsIURI* aContentLocation, nsILoadInfo* aLoadInfo, + const nsACString& aMimeGuess, bool aReportError, + int16_t* aDecision); + static void AccumulateMixedContentHSTS( + nsIURI* aURI, bool aActive, const OriginAttributes& aOriginAttributes); + + static bool URISafeToBeLoadedInSecureContext(nsIURI* aURI); + + static void OnPrefChange(const char* aPref, void* aClosure); + static void GetSecureContextAllowList(nsACString& aList); + static void Shutdown(); + + static bool sSecurecontextAllowlistCached; + static nsCString* sSecurecontextAllowlist; +}; + +#endif /* nsMixedContentBlocker_h___ */ diff --git a/dom/security/sanitizer/Sanitizer.cpp b/dom/security/sanitizer/Sanitizer.cpp new file mode 100644 index 0000000000..b302a76b7a --- /dev/null +++ b/dom/security/sanitizer/Sanitizer.cpp @@ -0,0 +1,173 @@ +/* -*- 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 "BindingDeclarations.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/DocumentFragment.h" +#include "mozilla/dom/SanitizerBinding.h" +#include "nsContentUtils.h" +#include "nsGenericHTMLElement.h" +#include "nsTreeSanitizer.h" +#include "Sanitizer.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Sanitizer, mGlobal) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Sanitizer) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Sanitizer) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Sanitizer) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +JSObject* Sanitizer::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return Sanitizer_Binding::Wrap(aCx, this, aGivenProto); +} + +/* static */ +already_AddRefed<Sanitizer> Sanitizer::Constructor( + const GlobalObject& aGlobal, const SanitizerConfig& aOptions, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<Sanitizer> sanitizer = new Sanitizer(global, aOptions); + return sanitizer.forget(); +} + +/* static */ +already_AddRefed<DocumentFragment> Sanitizer::InputToNewFragment( + const mozilla::dom::DocumentFragmentOrDocument& aInput, ErrorResult& aRv) { + // turns an DocumentFragmentOrDocument into a new DocumentFragment for + // internal use with nsTreeSanitizer + + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal); + if (!window || !window->GetDoc()) { + // FIXME: Should we throw another exception? + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + // We need to create a new docfragment based on the input + // and can't use a live document (possibly with mutation observershandlers) + nsAutoString innerHTML; + if (aInput.IsDocumentFragment()) { + RefPtr<DocumentFragment> inFragment = &aInput.GetAsDocumentFragment(); + inFragment->GetInnerHTML(innerHTML); + } else if (aInput.IsDocument()) { + RefPtr<Document> doc = &aInput.GetAsDocument(); + nsCOMPtr<Element> docElement = doc->GetDocumentElement(); + if (docElement) { + docElement->GetInnerHTML(innerHTML, IgnoreErrors()); + } + } + if (innerHTML.IsEmpty()) { + AutoTArray<nsString, 1> params = {}; + LogLocalizedString("SanitizerRcvdNoInput", params, + nsIScriptError::warningFlag); + + RefPtr<DocumentFragment> emptyFragment = + window->GetDoc()->CreateDocumentFragment(); + return emptyFragment.forget(); + } + // Create an inert HTML document, loaded as data. + // this ensures we do not cause any requests. + RefPtr<Document> emptyDoc = + nsContentUtils::CreateInertHTMLDocument(window->GetDoc()); + if (!emptyDoc) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + // We don't have a context element yet. let's create a mock HTML body element + RefPtr<mozilla::dom::NodeInfo> info = + emptyDoc->NodeInfoManager()->GetNodeInfo( + nsGkAtoms::body, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + + nsCOMPtr<nsINode> context = NS_NewHTMLBodyElement( + info.forget(), mozilla::dom::FromParser::FROM_PARSER_FRAGMENT); + RefPtr<DocumentFragment> fragment = nsContentUtils::CreateContextualFragment( + context, innerHTML, true /* aPreventScriptExecution */, aRv); + if (aRv.Failed()) { + aRv.ThrowInvalidStateError("Could not parse input"); + return nullptr; + } + return fragment.forget(); +} + +already_AddRefed<DocumentFragment> Sanitizer::Sanitize( + const mozilla::dom::DocumentFragmentOrDocument& aInput, ErrorResult& aRv) { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal); + if (!window || !window->GetDoc()) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + RefPtr<DocumentFragment> fragment = + Sanitizer::InputToNewFragment(aInput, aRv); + if (aRv.Failed()) { + return nullptr; + } + + mTreeSanitizer.Sanitize(fragment); + return fragment.forget(); +} + +RefPtr<DocumentFragment> Sanitizer::SanitizeFragment( + RefPtr<DocumentFragment> aFragment, ErrorResult& aRv) { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal); + if (!window || !window->GetDoc()) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + // FIXME(freddyb) + // (how) can we assert that the supplied doc is indeed inert? + mTreeSanitizer.Sanitize(aFragment); + return aFragment.forget(); +} + +/* ------ Logging ------ */ + +void Sanitizer::LogLocalizedString(const char* aName, + const nsTArray<nsString>& aParams, + uint32_t aFlags) { + uint64_t innerWindowID = 0; + bool isPrivateBrowsing = true; + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal); + if (window && window->GetDoc()) { + auto* doc = window->GetDoc(); + innerWindowID = doc->InnerWindowID(); + isPrivateBrowsing = nsContentUtils::IsInPrivateBrowsing(doc); + } + nsAutoString logMsg; + nsContentUtils::FormatLocalizedString(nsContentUtils::eSECURITY_PROPERTIES, + aName, aParams, logMsg); + LogMessage(logMsg, aFlags, innerWindowID, isPrivateBrowsing); +} + +/* static */ +void Sanitizer::LogMessage(const nsAString& aMessage, uint32_t aFlags, + uint64_t aInnerWindowID, bool aFromPrivateWindow) { + // Prepending 'Sanitizer' to the outgoing console message + nsString message; + message.AppendLiteral(u"Sanitizer: "); + message.Append(aMessage); + + // Allow for easy distinction in devtools code. + constexpr auto category = "Sanitizer"_ns; + + if (aInnerWindowID > 0) { + // Send to content console + nsContentUtils::ReportToConsoleByWindowID(message, aFlags, category, + aInnerWindowID); + } else { + // Send to browser console + nsContentUtils::LogSimpleConsoleError(message, category, aFromPrivateWindow, + true /* from chrome context */, + aFlags); + } +} + +} // namespace mozilla::dom diff --git a/dom/security/sanitizer/Sanitizer.h b/dom/security/sanitizer/Sanitizer.h new file mode 100644 index 0000000000..41b59c2ae2 --- /dev/null +++ b/dom/security/sanitizer/Sanitizer.h @@ -0,0 +1,104 @@ +/* -*- 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_Sanitizer_h +#define mozilla_dom_Sanitizer_h + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/DocumentFragment.h" +#include "mozilla/dom/SanitizerBinding.h" +#include "nsString.h" +#include "nsIGlobalObject.h" +#include "nsIParserUtils.h" +#include "nsTreeSanitizer.h" + +// XXX(Bug 1673929) This is not really needed here, but the generated +// SanitizerBinding.cpp needs it and does not include it. +#include "mozilla/dom/Document.h" + +class nsISupports; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class GlobalObject; + +class Sanitizer final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Sanitizer); + + explicit Sanitizer(nsIGlobalObject* aGlobal, const SanitizerConfig& aOptions) + : mGlobal(aGlobal), mTreeSanitizer(nsIParserUtils::SanitizerAllowStyle) { + MOZ_ASSERT(aGlobal); + mTreeSanitizer.WithWebSanitizerOptions(aGlobal, aOptions); + } + + nsIGlobalObject* GetParentObject() const { return mGlobal; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + /** + * Sanitizer() WebIDL constructor + * @return a new Sanitizer object, with methods as below + */ + static already_AddRefed<Sanitizer> Constructor( + const GlobalObject& aGlobal, const SanitizerConfig& aOptions, + ErrorResult& aRv); + + /** + * sanitize WebIDL method. + * @param aInput "bad" HTML that needs to be sanitized + * @return DocumentFragment of the sanitized HTML + */ + already_AddRefed<DocumentFragment> Sanitize( + const mozilla::dom::DocumentFragmentOrDocument& aInput, ErrorResult& aRv); + + /** + * Sanitizes a fragment in place. This assumes that the fragment + * belongs but an inert document. + * + * @param aFragment Fragment to be sanitized in place + * @return DocumentFragment + */ + + RefPtr<DocumentFragment> SanitizeFragment(RefPtr<DocumentFragment> aFragment, + ErrorResult& aRv); + + /** + * Logs localized message to either content console or browser console + * @param aName Localization key + * @param aParams Localization parameters + * @param aFlags Logging Flag (see nsIScriptError) + */ + void LogLocalizedString(const char* aName, const nsTArray<nsString>& aParams, + uint32_t aFlags); + + private: + ~Sanitizer() = default; + already_AddRefed<DocumentFragment> InputToNewFragment( + const mozilla::dom::DocumentFragmentOrDocument& aInput, ErrorResult& aRv); + /** + * Logs localized message to either content console or browser console + * @param aMessage Message to log + * @param aFlags Logging Flag (see nsIScriptError) + * @param aInnerWindowID Inner Window ID (Logged on browser console if 0) + * @param aFromPrivateWindow If from private window + */ + static void LogMessage(const nsAString& aMessage, uint32_t aFlags, + uint64_t aInnerWindowID, bool aFromPrivateWindow); + + RefPtr<nsIGlobalObject> mGlobal; + nsTreeSanitizer mTreeSanitizer; +}; +} // namespace dom +} // namespace mozilla + +#endif // ifndef mozilla_dom_Sanitizer_h diff --git a/dom/security/sanitizer/moz.build b/dom/security/sanitizer/moz.build new file mode 100644 index 0000000000..4baec486c5 --- /dev/null +++ b/dom/security/sanitizer/moz.build @@ -0,0 +1,37 @@ +# -*- 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Security") + +# TEST_DIRS += [ 'tests' ] + +MOCHITEST_MANIFESTS += ["tests/mochitest/mochitest.ini"] + + +EXPORTS.mozilla.dom += [ + "Sanitizer.h", +] + +UNIFIED_SOURCES += [ + "Sanitizer.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/base", + "/dom/bindings", + "/dom/html", +] + +# include('/ipc/chromium/chromium-config.mozbuild') +# include('/tools/fuzzing/libfuzzer-config.mozbuild') + +FINAL_LIBRARY = "xul" + +# if CONFIG['FUZZING_INTERFACES']: +# TEST_DIRS += [ +# 'fuzztest' +# ] diff --git a/dom/security/sanitizer/tests/mochitest/mochitest.ini b/dom/security/sanitizer/tests/mochitest/mochitest.ini new file mode 100644 index 0000000000..2388e0bd12 --- /dev/null +++ b/dom/security/sanitizer/tests/mochitest/mochitest.ini @@ -0,0 +1,6 @@ +[DEFAULT] +prefs = + dom.security.sanitizer.enabled=true + dom.security.setHTML.enabled=true +scheme=https +[test_sanitizer_api.html] diff --git a/dom/security/sanitizer/tests/mochitest/test_sanitizer_api.html b/dom/security/sanitizer/tests/mochitest/test_sanitizer_api.html new file mode 100644 index 0000000000..cfc24d0386 --- /dev/null +++ b/dom/security/sanitizer/tests/mochitest/test_sanitizer_api.html @@ -0,0 +1,139 @@ +<!DOCTYPE HTML> +<title>Test sanitizer api</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +<script type="text/javascript"> +"use strict"; +/* global Sanitizer */ +// we're not done after "onload" +SimpleTest.waitForExplicitFinish(); +(async function() { + // Ensure Sanitizer is not exposed when the pref is false + const isEnabled = SpecialPowers.getBoolPref("dom.security.sanitizer.enabled"); + if (!isEnabled) { + ok(false, "This test should only be run with dom.security.sanitizer.enabled set to true"); + SimpleTest.finish(); + } + + function* possibleInputTypes(inputStr) { + /* This generator function, given a string, yields all possible input objects + for our sanitizer API (string, docfragment, document). + */ + + // 1) as string + yield ({testInput: inputStr, testType: "String" }); + // 2) as DocumentFragment + let temp = document.createElement('template'); + // asking eslint to skip this: innerHTML is safe for template elements. + // eslint-disable-next-line no-unsanitized/property + temp.innerHTML = inputStr; + yield ({testInput: temp.content, testType: "DocumentFragment" }); + // 3) as HTMLDocument + const parser = new DOMParser; + yield ({testInput: parser.parseFromString(inputStr, "text/html"), testType: "Document" }); + } + // basic interface smoke test + ok(typeof Sanitizer === "function", "Sanitizer constructor exposed when preffed on"); + const mySanitizer = new Sanitizer(); + ok(mySanitizer, "Sanitizer constructor works"); + ok(mySanitizer.sanitize, "sanitize function exists"); + ok("setHTML" in Element.prototype, "Element.setHTML exists"); + + // testing sanitizer results + const testCases = [ + { + testString: "<p>hello</p>", + testExpected: "<p>hello</p>", + sanitizerOptions: {} + }, + { + // script element encoded to not confuse the HTML parser and end execution here + testString: "<p>second test</p><script>alert(1)\x3C/script>", + testExpected: "<p>second test</p>", + sanitizerOptions: {}, + }, + { + // test for the allowElements option + testString: "<p>hello <i>folks</i></p>", + testExpected: "<p>hello folks</p>", + sanitizerOptions: { allowElements: ["p"] }, + }, + { + // test for the blockElements option + testString: "<p>hello <i>folks</i></p>", + testExpected: "<p>hello folks</p>", + sanitizerOptions: { blockElements: ["i"] }, + }, + // TODO: Unknown attributes aren't supported yet. + // { + // // test for the allowAttributes option + // testString: `<p haha="lol">hello</p>`, + // testExpected: `<p haha="lol">hello</p>`, + // sanitizerOptions: { allowUnknownMarkup: true, allowAttributes: { 'haha': ['p'] } }, + // }, + { + // confirming the inverse + testString: `<p haha="lol">hello</p>`, + testExpected: `<p>hello</p>`, + sanitizerOptions: {}, + }, + { + // test for the dropAttributes option + testString: `<p title="dropme">hello</p>`, + testExpected: `<p>hello</p>`, + sanitizerOptions: { dropAttributes: { 'title': ['p'] } }, + }, + { + // confirming the inverse + testString: `<p title="dontdropme">hello</p>`, + testExpected: `<p title="dontdropme">hello</p>`, + sanitizerOptions: {}, + }, + { + // if an attribute is allowed and dropped, the drop will take preference + testString: `<p title="lol">hello</p>`, + testExpected: `<p>hello</p>`, + sanitizerOptions: { + allowAttributes: { 'title': ['p'] }, + dropAttributes: { 'title': ['p'] }, + }, + }, + ]; + + + const div = document.createElement("div"); + for (let test of testCases) { + const {testString, testExpected, sanitizerOptions} = test; + const testSanitizer = new Sanitizer(sanitizerOptions); + + for (let testInputAndType of possibleInputTypes(testString)) { + const {testInput, testType} = testInputAndType; + + if (testType != "String") { + // test sanitize(document/fragment) + try { + div.innerHTML = ""; + const docFragment = testSanitizer.sanitize(testInput); + div.append(docFragment); + is(div.innerHTML, testExpected, `Sanitizer.sanitize() should turn (${testType}) '${testInput}' into '${testExpected}'`); + } + catch (e) { + ok(false, 'Error in sanitize() test: ' + e) + } + } + else { + // test setHTML: + try { + div.setHTML(testString, { sanitizer: testSanitizer }); + is(div.innerHTML, testExpected, `div.setHTML() should turn(${testType}) '${testInput}' into '${testExpected}'`); + } + catch (e) { + ok(false, 'Error in setHTML() test: ' + e) + } + } + } + } + + SimpleTest.finish(); +})(); +</script> diff --git a/dom/security/test/cors/browser.ini b/dom/security/test/cors/browser.ini new file mode 100644 index 0000000000..350d382e34 --- /dev/null +++ b/dom/security/test/cors/browser.ini @@ -0,0 +1,9 @@ +[DEFAULT] +support-files = + file_CrossSiteXHR_server.sjs + file_CrossSiteXHR_inner.html + file_cors_logging_test.html + file_bug1456721.html + bug1456721.sjs + +[browser_CORS-console-warnings.js] diff --git a/dom/security/test/cors/browser_CORS-console-warnings.js b/dom/security/test/cors/browser_CORS-console-warnings.js new file mode 100644 index 0000000000..0c90ee3086 --- /dev/null +++ b/dom/security/test/cors/browser_CORS-console-warnings.js @@ -0,0 +1,101 @@ +/* + * Description of the test: + * Ensure that CORS warnings are printed to the web console. + * + * This test uses the same tests as the plain mochitest, but needs access to + * the console. + */ +"use strict"; + +function console_observer(subject, topic, data) { + var message = subject.wrappedJSObject.arguments[0]; + ok(false, message); +} + +var webconsole = null; +var messages_seen = 0; +var expected_messages = 50; + +function on_new_message(msgObj) { + let text = msgObj.message; + + if (text.match("Cross-Origin Request Blocked:")) { + ok(true, "message is: " + text); + messages_seen++; + } +} + +async function do_cleanup() { + Services.console.unregisterListener(on_new_message); + await unsetCookiePref(); +} + +/** + * Set e10s related preferences in the test environment. + * @return {Promise} promise that resolves when preferences are set. + */ +function setCookiePref() { + return new Promise(resolve => + // accept all cookies so that the CORS requests will send the right cookies + SpecialPowers.pushPrefEnv( + { + set: [["network.cookie.cookieBehavior", 0]], + }, + resolve + ) + ); +} + +/** + * Unset e10s related preferences in the test environment. + * @return {Promise} promise that resolves when preferences are unset. + */ +function unsetCookiePref() { + return new Promise(resolve => { + SpecialPowers.popPrefEnv(resolve); + }); +} + +//jscs:disable +add_task(async function() { + //jscs:enable + // A longer timeout is necessary for this test than the plain mochitests + // due to opening a new tab with the web console. + requestLongerTimeout(4); + registerCleanupFunction(do_cleanup); + await setCookiePref(); + Services.console.registerListener(on_new_message); + + let test_uri = + "http://mochi.test:8888/browser/dom/security/test/cors/file_cors_logging_test.html"; + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + BrowserTestUtils.loadURI(gBrowser, test_uri); + + await BrowserTestUtils.waitForLocationChange( + gBrowser, + test_uri + "#finished" + ); + + // Different OS combinations + ok(messages_seen > 0, "Saw " + messages_seen + " messages."); + + messages_seen = 0; + let test_two_uri = + "http://mochi.test:8888/browser/dom/security/test/cors/file_bug1456721.html"; + BrowserTestUtils.loadURI(gBrowser, test_two_uri); + + await BrowserTestUtils.waitForLocationChange( + gBrowser, + test_two_uri + "#finishedTestTwo" + ); + await BrowserTestUtils.waitForCondition(() => messages_seen > 0); + + ok(messages_seen > 0, "Saw " + messages_seen + " messages."); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/security/test/cors/bug1456721.sjs b/dom/security/test/cors/bug1456721.sjs new file mode 100644 index 0000000000..de8bd5a7f4 --- /dev/null +++ b/dom/security/test/cors/bug1456721.sjs @@ -0,0 +1,20 @@ +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + let queryStr = request.queryString; + + if (queryStr === "redirect") { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", "bug1456721.sjs?load", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + return; + } + + if (queryStr === "load") { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.write("foo"); + return; + } + // we should never get here - return something unexpected + response.write("d'oh"); +} 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 0000000000..3e8f291327 --- /dev/null +++ b/dom/security/test/cors/file_CrossSiteXHR_cache_server.sjs @@ -0,0 +1,59 @@ +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 = JSON.parse( + 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 0000000000..d3e8421362 --- /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 = JSON.parse(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); + }); + } + xhr.addEventListener("readystatechange", function(e) { + res.events.push("rs" + xhr.readyState); + }); + xhr.addEventListener("progress", function(e) { + res.progressEvents++; + }); + if (req.uploadProgress) { + xhr.upload.addEventListener(req.uploadProgress, function(e) { + res.progressEvents++; + }); + } + 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 : ""); + } 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); + } + +}); + +function post(e, res) { + e.source.postMessage(JSON.stringify(res), "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 0000000000..bdb0eb4408 --- /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 0000000000..4a030c4211 --- /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 = JSON.parse(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(JSON.stringify(res), "*");\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 0000000000..a2e93ba029 --- /dev/null +++ b/dom/security/test/cors/file_CrossSiteXHR_server.sjs @@ -0,0 +1,231 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + +// eslint-disable-next-line complexity +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 = JSON.parse(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 = JSON.parse(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 = JSON.parse(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/file_bug1456721.html b/dom/security/test/cors/file_bug1456721.html new file mode 100644 index 0000000000..20ef00d06d --- /dev/null +++ b/dom/security/test/cors/file_bug1456721.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<head> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8"> + <title>Test new CORS console messages</title> +</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"> + +let gen; +let number_of_tests = 0; + +function initTest() { + window.addEventListener("message", function(e) { + gen.next(e.data); + if (number_of_tests == 2) { + document.location.href += "#finishedTestTwo"; + } + }); + + gen = runTest(); + + gen.next(); +} + +function* runTest() { + let loader = document.getElementById("loader"); + let loaderWindow = loader.contentWindow; + loader.onload = function() { gen.next(); }; + + loader.src = "http://example.org/browser/dom/security/test/cors/file_CrossSiteXHR_inner.html"; + origin = "http://example.org"; + yield undefined; + + let tests = [ + // Loading URLs other than http(s) should throw 'CORS request + // not http' console message. + { baseURL: "ftp://mochi.test:8888/browser/dom/security/test/cors/file_CrossSiteXHR_server.sjs", + method: "GET", + }, + // (https://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0) + // CORs preflight external redirect should throw 'CORS request + // external redirect not allowed' error. + // This will also throw 'CORS preflight channel did not succeed' + // and 'CORS request did not succeed' console messages. + { + baseURL: "http://mochi.test:8888/browser/dom/security/test/cors/bug1456721.sjs?redirect", + method: "OPTIONS", + }, + ]; + + for (let test of tests) { + let req = { + url: test.baseURL, + method: test.method + }; + + loaderWindow.postMessage(JSON.stringify(req), origin); + number_of_tests++; + yield; + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/cors/file_cors_logging_test.html b/dom/security/test/cors/file_cors_logging_test.html new file mode 100644 index 0000000000..d29f93cf9c --- /dev/null +++ b/dom/security/test/cors/file_cors_logging_test.html @@ -0,0 +1,1311 @@ +<!DOCTYPE HTML> +<html> +<head> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8"> + <title>Test for Cross Site XMLHttpRequest</title> +</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"> + +const runPreflightTests = 1; +const runCookieTests = 1; +const runRedirectTests = 1; + +var gen; + +function initTest() { + window.addEventListener("message", function(e) { + gen.next(e.data); + }); + + gen = runTest(); + + gen.next() +} + +function initTestCallback() { +} + +function* runTest() { + var loader = document.getElementById('loader'); + var loaderWindow = loader.contentWindow; + loader.onload = function () { gen.next() }; + + // Test preflight-less requests + basePath = "/browser/dom/security/test/cors/file_CrossSiteXHR_server.sjs?" + baseURL = "http://mochi.test:8888" + basePath; + + // Test preflighted requests + loader.src = "http://example.org/browser/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"] + .includes(test.headers[name].toLowerCase())); + } + req.url += "&headers=" + escape(JSON.stringify(test.headers)); + reqHeaders = + escape(Object.keys(test.headers) + .filter(isUnsafeHeader) + .map(s => s.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(JSON.stringify(test.responseHeaders)); + if (test.exposeHeaders) + req.url += "&exposeHeaders=" + escape(test.exposeHeaders); + if (test.preflightBody) + req.url += "&preflightBody=" + escape(test.preflightBody); + + loaderWindow.postMessage(JSON.stringify(req), origin); + res = JSON.parse(yield); + } + + // 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(JSON.stringify(req), origin); + + res = JSON.parse(yield); + } + + // Make sure to clear cookies to avoid affecting other tests + document.cookie = "a=; path=/; expires=Thu, 01-Jan-1970 00:00:01 GMT" + + // Test redirects + + 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(JSON.stringify(test.hops)), + 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(JSON.stringify(req), origin); + + res = JSON.parse(yield); + } + + document.location.href += "#finished"; +} + +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/cors/file_cors_logging_test.html.css b/dom/security/test/cors/file_cors_logging_test.html.css new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/security/test/cors/file_cors_logging_test.html.css diff --git a/dom/security/test/cors/mochitest.ini b/dom/security/test/cors/mochitest.ini new file mode 100644 index 0000000000..7dcdc05c71 --- /dev/null +++ b/dom/security/test/cors/mochitest.ini @@ -0,0 +1,10 @@ +[DEFAULT] +support-files = + file_CrossSiteXHR_cache_server.sjs + file_CrossSiteXHR_inner.html + 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 0000000000..45929a8c2d --- /dev/null +++ b/dom/security/test/cors/test_CrossSiteXHR.html @@ -0,0 +1,1491 @@ +<!DOCTYPE HTML> +<html> +<head> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8"> + <title>Test for Cross Site XMLHttpRequest</title> + <script 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"> + +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], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ] + }, initTestCallback); +} + +function initTestCallback() { + window.addEventListener("message", function(e) { + gen.next(e.data); + }); + + gen = runTest(); + + gen.next() +} + +// eslint-disable-next-line complexity +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: 1, + method: "GET", + noAllowPreflight: 1, + username: "user", + }, + + // nonempty password + { 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"] + .includes(test.headers[name].toLowerCase())); + } + req.url += "&headers=" + escape(JSON.stringify(test.headers)); + reqHeaders = + escape(Object.keys(test.headers) + .filter(isUnsafeHeader) + .map(s => s.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(JSON.stringify(test.responseHeaders)); + if (test.exposeHeaders) + req.url += "&exposeHeaders=" + escape(test.exposeHeaders); + if (test.preflightBody) + req.url += "&preflightBody=" + escape(test.preflightBody); + + loaderWindow.postMessage(JSON.stringify(req), origin); + res = JSON.parse(yield); + + if (test.pass) { + is(res.didFail, false, + "shouldn't have failed in test for " + JSON.stringify(test)); + if (test.status) { + is(res.status, test.status, "wrong status in test for " + JSON.stringify(test)); + is(res.statusText, test.statusMessage, "wrong status text for " + JSON.stringify(test)); + } + else { + is(res.status, 200, "wrong status in test for " + JSON.stringify(test)); + is(res.statusText, "OK", "wrong status text for " + JSON.stringify(test)); + } + if (test.method !== "HEAD") { + is(res.responseXML, "<res>hello pass</res>", + "wrong responseXML in test for " + JSON.stringify(test)); + is(res.responseText, "<res>hello pass</res>\n", + "wrong responseText in test for " + JSON.stringify(test)); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs2,rs3,rs4,load,loadend", + "wrong responseText in test for " + JSON.stringify(test)); + } + else { + is(res.responseXML, null, + "wrong responseXML in test for " + JSON.stringify(test)); + is(res.responseText, "", + "wrong responseText in test for " + JSON.stringify(test)); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs2,rs4,load,loadend", + "wrong responseText in test for " + JSON.stringify(test)); + } + if (test.responseHeaders) { + for (header in test.responseHeaders) { + if (!test.expectedResponseHeaders.includes(header)) { + is(res.responseHeaders[header], null, + "|xhr.getResponseHeader()|wrong response header (" + header + ") in test for " + + JSON.stringify(test)); + is(res.allResponseHeaders[header], undefined, + "|xhr.getAllResponseHeaderss()|wrong response header (" + header + ") in test for " + + JSON.stringify(test)); + } + else { + is(res.responseHeaders[header], test.responseHeaders[header], + "|xhr.getResponseHeader()|wrong response header (" + header + ") in test for " + + JSON.stringify(test)); + is(res.allResponseHeaders[header.toLowerCase()], test.responseHeaders[header], + "|xhr.getAllResponseHeaderss()|wrong response header (" + header + ") in test for " + + JSON.stringify(test)); + } + } + } + } + else { + is(res.didFail, true, + "should have failed in test for " + JSON.stringify(test)); + is(res.status, 0, "wrong status in test for " + JSON.stringify(test)); + is(res.statusText, "", "wrong status text for " + JSON.stringify(test)); + is(res.responseXML, null, + "wrong responseXML in test for " + JSON.stringify(test)); + is(res.responseText, "", + "wrong responseText in test for " + JSON.stringify(test)); + if (!res.sendThrew) { + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs4,error,loadend", + "wrong events in test for " + JSON.stringify(test)); + } + is(res.progressEvents, 0, + "wrong events in test for " + JSON.stringify(test)); + if (test.responseHeaders) { + for (header in test.responseHeaders) { + is(res.responseHeaders[header], null, + "wrong response header (" + header + ") in test for " + + JSON.stringify(test)); + } + } + } + } + + // 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(JSON.stringify(req), origin); + + res = JSON.parse(yield); + if (test.pass) { + is(res.didFail, false, + "shouldn't have failed in test for " + JSON.stringify(test)); + is(res.status, 200, "wrong status in test for " + JSON.stringify(test)); + is(res.statusText, "OK", "wrong status text for " + JSON.stringify(test)); + is(res.responseXML, "<res>hello pass</res>", + "wrong responseXML in test for " + JSON.stringify(test)); + is(res.responseText, "<res>hello pass</res>\n", + "wrong responseText in test for " + JSON.stringify(test)); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs2,rs3,rs4,load,loadend", + "wrong responseText in test for " + JSON.stringify(test)); + } + else { + is(res.didFail, true, + "should have failed in test for " + JSON.stringify(test)); + is(res.status, 0, "wrong status in test for " + JSON.stringify(test)); + is(res.statusText, "", "wrong status text for " + JSON.stringify(test)); + is(res.responseXML, null, + "wrong responseXML in test for " + JSON.stringify(test)); + is(res.responseText, "", + "wrong responseText in test for " + JSON.stringify(test)); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs4,error,loadend", + "wrong events in test for " + JSON.stringify(test)); + is(res.progressEvents, 0, + "wrong events in test for " + JSON.stringify(test)); + } + } + + // 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: 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", + }, + { server: "http://sub1.test1.example.org", + allowOrigin: "*", + allowHeaders: "my-header", + }, + ], + }, + { 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", + }, + { 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: 1, + method: "DELETE", + hops: [{ server: "http://example.org", + }, + { server: "http://example.com", + allowOrigin: origin, + allowMethods: "DELETE", + }, + { server: "http://sub1.test1.example.org", + allowOrigin: "*", + allowMethods: "DELETE", + }, + ], + }, + { pass: 1, + 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(JSON.stringify(test.hops)), + 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(JSON.stringify(req), origin); + + res = JSON.parse(yield); + if (test.pass) { + is(res.didFail, false, + "shouldn't have failed in test for " + JSON.stringify(test)); + is(res.status, 200, "wrong status in test for " + JSON.stringify(test)); + is(res.statusText, "OK", "wrong status text for " + JSON.stringify(test)); + is(res.responseXML, "<res>hello pass</res>", + "wrong responseXML in test for " + JSON.stringify(test)); + is(res.responseText, "<res>hello pass</res>\n", + "wrong responseText in test for " + JSON.stringify(test)); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs2,rs3,rs4,load,loadend", + "wrong responseText in test for " + JSON.stringify(test)); + } + else { + is(res.didFail, true, + "should have failed in test for " + JSON.stringify(test)); + is(res.status, 0, "wrong status in test for " + JSON.stringify(test)); + is(res.statusText, "", "wrong status text for " + JSON.stringify(test)); + is(res.responseXML, null, + "wrong responseXML in test for " + JSON.stringify(test)); + is(res.responseText, "", + "wrong responseText in test for " + JSON.stringify(test)); + is(res.events.join(","), + "opening,rs1,sending,loadstart,rs4,error,loadend", + "wrong events in test for " + JSON.stringify(test)); + is(res.progressEvents, 0, + "wrong progressevents in test for " + JSON.stringify(test)); + } + } + + + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + SimpleTest.finish(); +} + +</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 0000000000..77898e38ed --- /dev/null +++ b/dom/security/test/cors/test_CrossSiteXHR_cache.html @@ -0,0 +1,610 @@ +<!DOCTYPE HTML> +<html> +<head> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8"> + <title>Test for Cross Site XMLHttpRequest</title> + <script 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"> + +let gen; +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.next(e.data); +}); + +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: 1, + 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: 0, + method: "GET", + headers: { "x-my-header": "myValue", + "y-my-header": "second" }, + }, + { newTest: "*******" }, + { pass: 1, + method: "GET", + headers: { "y-my-header": "hello" }, + allowHeaders: "y-my-header,x-my-header", + }, + { pass: 1, + method: "GET", + headers: { "y-my-header": "hello" }, + }, + { 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: 1, + method: "PATCH", + }, + { pass: 1, + method: "PATCH", + allowMethods: "PATCH", + cacheTime: 3600, + }, + { pass: 1, + method: "PATCH", + }, + { pass: 0, + method: "DELETE", + }, + { pass: 0, + method: "PUT", + }, + { newTest: "*******" }, + { pass: 1, + method: "PATCH", + allowMethods: "PATCH", + cacheTime: 3600, + }, + { pass: 1, + method: "PATCH", + }, + { 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(JSON.stringify(sec)), true); + xhr.onloadend = function() { gen.next(); } + xhr.send(); + yield undefined; + + loaderWindow.postMessage(JSON.stringify(req), origin); + + res = JSON.parse(yield); + + testName = JSON.stringify(test) + " (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,rs4,error,loadend", + "wrong events in test for " + testName); + is(res.progressEvents, 0, + "wrong events in test for " + testName); + } + } + + SimpleTest.finish(); +} + +</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 0000000000..31cc5809d5 --- /dev/null +++ b/dom/security/test/cors/test_CrossSiteXHR_origin.html @@ -0,0 +1,167 @@ +<!DOCTYPE HTML> +<html> +<head> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8"> + <title>Test for Cross Site XMLHttpRequest</title> + <script 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"> + +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: '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.next(e.data); +}); + +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(JSON.stringify(req), isNullOrigin ? "*" : origin); + + res = JSON.parse(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(JSON.stringify(req), isNullOrigin ? "*" : origin); + + res = JSON.parse(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,rs4,error,loadend", + "wrong events in test for " + allowOrigin); + is(res.progressEvents, 0, + "wrong events in test for " + allowOrigin); + } + } + + SimpleTest.finish(); +} + +addLoadEvent(function() { + gen.next(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/security/test/crashtests/1577572.html b/dom/security/test/crashtests/1577572.html new file mode 100644 index 0000000000..732c7aa5dc --- /dev/null +++ b/dom/security/test/crashtests/1577572.html @@ -0,0 +1,10 @@ +<html> +<title>Bug 1577572</title> +<head> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content="ÿ"> +</head> +<body> + Bug 1577572 +</body> +</html> diff --git a/dom/security/test/crashtests/1583044.html b/dom/security/test/crashtests/1583044.html new file mode 100644 index 0000000000..aa6d496d64 --- /dev/null +++ b/dom/security/test/crashtests/1583044.html @@ -0,0 +1,11 @@ +<html> +<head> +<title>Bug 1583044</title> +<script> + function testOpenMozIcon() { + window.location.href = "moz-icon://.pdf?size=128"; + } +</script> +</head> +<body onload="testOpenMozIcon();"></body> +</html> diff --git a/dom/security/test/crashtests/crashtests.list b/dom/security/test/crashtests/crashtests.list new file mode 100644 index 0000000000..fc7986cf3d --- /dev/null +++ b/dom/security/test/crashtests/crashtests.list @@ -0,0 +1,2 @@ +load 1583044.html +load 1577572.html diff --git a/dom/security/test/csp/Ahem.ttf b/dom/security/test/csp/Ahem.ttf Binary files differnew file mode 100644 index 0000000000..ac81cb0316 --- /dev/null +++ b/dom/security/test/csp/Ahem.ttf diff --git a/dom/security/test/csp/File b/dom/security/test/csp/File new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/security/test/csp/File diff --git a/dom/security/test/csp/browser.ini b/dom/security/test/csp/browser.ini new file mode 100644 index 0000000000..f2273f0180 --- /dev/null +++ b/dom/security/test/csp/browser.ini @@ -0,0 +1,23 @@ +[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 + file_test_browser_bookmarklets.html + file_test_browser_bookmarklets.html^headers^ +[browser_test_web_manifest.js] +[browser_test_web_manifest_mixed_content.js] +[browser_test_bookmarklets.js] +[browser_test_uir_optional_clicks.js] +support-files = + file_csp_meta_uir.html +[browser_manifest-src-override-default-src.js] +[browser_pdfjs_not_subject_to_csp.js] +support-files = + dummy.pdf + file_pdfjs_not_subject_to_csp.html 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 0000000000..cd2b0f4460 --- /dev/null +++ b/dom/security/test/csp/browser_manifest-src-override-default-src.js @@ -0,0 +1,125 @@ +/* + * Description of the tests: + * Tests check that default-src can be overridden by manifest-src. + */ +/*globals Cu, is, ok*/ +"use strict"; +const { ManifestObtainer } = ChromeUtils.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(`https://example.org${server}`); +const mixedURL = new URL(`http://mochi.test:8888${server}`); + +// Enable web manifest processing. +Services.prefs.setBoolPref("dom.manifest.enabled", true); + +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(async function() { + //jscs:enable + const testPromises = tests.map(test => { + const tabOptions = { + gBrowser, + url: test.tabURL, + skipAnimation: true, + }; + return BrowserTestUtils.withNewTab(tabOptions, browser => + testObtainingManifest(browser, test) + ); + }); + await Promise.all(testPromises); +}); + +async 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 = await 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) { + await 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"); + return observer; +} diff --git a/dom/security/test/csp/browser_pdfjs_not_subject_to_csp.js b/dom/security/test/csp/browser_pdfjs_not_subject_to_csp.js new file mode 100644 index 0000000000..dbcbf8efbf --- /dev/null +++ b/dom/security/test/csp/browser_pdfjs_not_subject_to_csp.js @@ -0,0 +1,48 @@ +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [["pdfjs.eventBusDispatchToDOM", true]], + }); + await BrowserTestUtils.withNewTab( + TEST_PATH + "file_pdfjs_not_subject_to_csp.html", + async function(browser) { + let pdfPromise = BrowserTestUtils.waitForContentEvent( + browser, + "documentloaded", + false, + null, + true + ); + + await ContentTask.spawn(browser, {}, async function() { + let pdfButton = content.document.getElementById("pdfButton"); + pdfButton.click(); + }); + + await pdfPromise; + + await ContentTask.spawn(browser, {}, async function() { + let pdfFrame = content.document.getElementById("pdfFrame"); + // 1) Sanity that we have loaded the PDF using a blob + ok(pdfFrame.src.startsWith("blob:"), "it's a blob URL"); + + // 2) Ensure that the PDF has actually loaded + ok( + pdfFrame.contentDocument.querySelector("div#viewer"), + "document content has viewer UI" + ); + + // 3) Ensure we have the correct CSP attached + let cspJSON = pdfFrame.contentDocument.cspJSON; + ok(cspJSON.includes("script-src"), "found script-src directive"); + ok(cspJSON.includes("allowPDF"), "found script-src nonce value"); + }); + } + ); +}); diff --git a/dom/security/test/csp/browser_test_bookmarklets.js b/dom/security/test/csp/browser_test_bookmarklets.js new file mode 100644 index 0000000000..d5bf8cf79d --- /dev/null +++ b/dom/security/test/csp/browser_test_bookmarklets.js @@ -0,0 +1,82 @@ +"use strict"; + +let BASE_URL = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "https://example.com/" +); +const DUMMY_URL = BASE_URL + "file_test_browser_bookmarklets.html"; + +function makeBookmarkFor(url, keyword) { + return Promise.all([ + PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmarklet", + url, + }), + PlacesUtils.keywords.insert({ url, keyword }), + ]); +} +/* Test Description: + * 1 - Load a Page with CSP script-src: none + * 2 - Create a bookmarklet with javascript:window.open('about:blank') + * 3 - Select and enter the bookmarklet + * A new tab with about:blank should be opened + */ +add_task(async function openKeywordBookmarkWithWindowOpen() { + // This is the current default, but let's not assume that... + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.link.open_newwindow", 3], + ["dom.disable_open_during_load", true], + ], + }); + + let moztab; + let tabOpened = BrowserTestUtils.openNewForegroundTab( + gBrowser, + DUMMY_URL + ).then(tab => { + moztab = tab; + }); + let keywordForBM = "openNewWindowBookmarklet"; + + let bookmarkInfo; + let bookmarkCreated = makeBookmarkFor( + `javascript: window.open("about:blank")`, + keywordForBM + ).then(values => { + bookmarkInfo = values[0]; + }); + await Promise.all([tabOpened, bookmarkCreated]); + + registerCleanupFunction(function() { + return Promise.all([ + PlacesUtils.bookmarks.remove(bookmarkInfo), + PlacesUtils.keywords.remove(keywordForBM), + ]); + }); + gURLBar.value = keywordForBM; + gURLBar.focus(); + + let tabCreatedPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + EventUtils.synthesizeKey("KEY_Enter"); + info("Waiting for tab being created"); + let { target: tab } = await tabCreatedPromise; + info("Got tab"); + let browser = tab.linkedBrowser; + if (!browser.currentURI || browser.currentURI.spec != "about:blank") { + info("Waiting for browser load"); + await BrowserTestUtils.browserLoaded(browser, false, "about:blank"); + } + is( + browser.currentURI && browser.currentURI.spec, + "about:blank", + "Tab with expected URL loaded." + ); + info("Waiting to remove tab"); + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(moztab); +}); diff --git a/dom/security/test/csp/browser_test_uir_optional_clicks.js b/dom/security/test/csp/browser_test_uir_optional_clicks.js new file mode 100644 index 0000000000..05cd15dd04 --- /dev/null +++ b/dom/security/test/csp/browser_test_uir_optional_clicks.js @@ -0,0 +1,36 @@ +"use strict"; + +const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); +const TEST_PATH_HTTPS = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", false]], + }); + await BrowserTestUtils.withNewTab( + TEST_PATH_HTTPS + "file_csp_meta_uir.html", + async function(browser) { + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true); + BrowserTestUtils.synthesizeMouse( + "#mylink", + 2, + 2, + { accelKey: true }, + browser + ); + let tab = await newTabPromise; + is( + tab.linkedBrowser.currentURI.scheme, + "https", + "Should have opened https page." + ); + BrowserTestUtils.removeTab(tab); + } + ); +}); 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 0000000000..02736f3c08 --- /dev/null +++ b/dom/security/test/csp/browser_test_web_manifest.js @@ -0,0 +1,239 @@ +/* + * 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 } = ChromeUtils.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}`); + +// Enable web manifest processing. +Services.prefs.setBoolPref("dom.manifest.enabled", true); + +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(async function() { + //jscs:enable + const testPromises = tests.map(test => { + const tabOptions = { + gBrowser, + url: test.tabURL, + skipAnimation: true, + }; + return BrowserTestUtils.withNewTab(tabOptions, browser => + testObtainingManifest(browser, test) + ); + }); + await Promise.all(testPromises); +}); + +async function testObtainingManifest(aBrowser, aTest) { + const waitForObserver = waitForNetObserver(aBrowser, aTest); + // Expect an exception (from promise rejection) if there a content policy + // that is violated. + try { + const manifest = await 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 { + await waitForObserver; + } +} + +// Helper object used to observe policy violations when blocking is expected. +function waitForNetObserver(aBrowser, aTest) { + // We don't need to wait for violation, so just resolve + if (!aTest.expected.includes("block")) { + return Promise.resolve(); + } + + return ContentTask.spawn(aBrowser, [], () => { + return new Promise(resolve => { + function observe(subject, topic) { + Services.obs.removeObserver(observe, "csp-on-violate-policy"); + resolve(); + } + Services.obs.addObserver(observe, "csp-on-violate-policy"); + }); + }).then(() => aTest.run("csp-on-violate-policy")); +} 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 0000000000..9c1d2a594f --- /dev/null +++ b/dom/security/test/csp/browser_test_web_manifest_mixed_content.js @@ -0,0 +1,57 @@ +/* + * Description of the test: + * Check that mixed content blocker works prevents fetches of + * mixed content manifests. + */ +/*globals Cu, ok*/ +"use strict"; +const { ManifestObtainer } = ChromeUtils.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(async function() { + //jscs:enable + const testPromises = tests.map(test => { + const tabOptions = { + gBrowser, + url: test.tabURL, + skipAnimation: true, + }; + return BrowserTestUtils.withNewTab(tabOptions, browser => + testObtainingManifest(browser, test) + ); + }); + await Promise.all(testPromises); +}); + +async function testObtainingManifest(aBrowser, aTest) { + try { + await ManifestObtainer.browserObtainManifest(aBrowser); + } catch (e) { + aTest.run(e); + } +} diff --git a/dom/security/test/csp/dummy.pdf b/dom/security/test/csp/dummy.pdf Binary files differnew file mode 100644 index 0000000000..7ad87e3c2e --- /dev/null +++ b/dom/security/test/csp/dummy.pdf diff --git a/dom/security/test/csp/file_CSP.css b/dom/security/test/csp/file_CSP.css new file mode 100644 index 0000000000..6835c4d4ad --- /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 0000000000..834f7e13ea --- /dev/null +++ b/dom/security/test/csp/file_CSP.sjs @@ -0,0 +1,24 @@ +// 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 0000000000..787e683e87 --- /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 0000000000..ba130e99e4 --- /dev/null +++ b/dom/security/test/csp/file_base_uri_server.sjs @@ -0,0 +1,59 @@ +// 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 0000000000..0a4a491606 --- /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_blob_top_nav_block_modals.html b/dom/security/test/csp/file_blob_top_nav_block_modals.html new file mode 100644 index 0000000000..545f6cffff --- /dev/null +++ b/dom/security/test/csp/file_blob_top_nav_block_modals.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> +<script> + // If the alert box is blocked correctly by the CSP then postMessage will + // send the message and test passes. + var text = "<script>alert(document.domain);window.opener.postMessage("+ + "{\"test\": \"block_top_nav_alert_test\", \"msg\": "+ + "\"blob top nav alert blocked by CSP\"}, \"*\")<\/script>"; + var blob = new Blob([text], {type : 'text/html'}); + var url = URL.createObjectURL(blob); + location.href=url; +</script> +</body> +</html>
\ No newline at end of file diff --git a/dom/security/test/csp/file_blob_top_nav_block_modals.html^headers^ b/dom/security/test/csp/file_blob_top_nav_block_modals.html^headers^ new file mode 100644 index 0000000000..e2d945d556 --- /dev/null +++ b/dom/security/test/csp/file_blob_top_nav_block_modals.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: sandbox allow-scripts;
\ No newline at end of file diff --git a/dom/security/test/csp/file_blob_uri_blocks_modals.html b/dom/security/test/csp/file_blob_uri_blocks_modals.html new file mode 100644 index 0000000000..caf2a5de41 --- /dev/null +++ b/dom/security/test/csp/file_blob_uri_blocks_modals.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> +<!-- iframe loading the blob url with null origin --> +<iframe id="blobFrame"></iframe> +<script> + // If the alert box is blocked correctly by the CSP then postMessage will + // send the message and test passes. + var alertScriptText = "data:text/html,<script>location=URL.createObjectURL(" + + "new Blob(['<script>alert(document.URL);parent.parent.postMessage(" + + "{\"test\": \"block_alert_test\", \"msg\": \"alert blocked by" + + " CSP\"}, \"*\");<\\/script>'], {type:\"text/html\"}));<\/script>"; + document.getElementById("blobFrame").src=alertScriptText; + try { + var w = window.open("http://www.example.com","newwindow"); + parent.postMessage({"test": "block_window_open_test", + "msg": "new window not blocked by CSP"},"*"); + } catch(err) { + parent.postMessage({"test": "block_window_open_test", + "msg": "window blocked by CSP"},"*"); + } +</script> +</body> +</html> diff --git a/dom/security/test/csp/file_blob_uri_blocks_modals.html^headers^ b/dom/security/test/csp/file_blob_uri_blocks_modals.html^headers^ new file mode 100644 index 0000000000..e2d945d556 --- /dev/null +++ b/dom/security/test/csp/file_blob_uri_blocks_modals.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: sandbox allow-scripts;
\ No newline at end of file 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 0000000000..003c9df57c --- /dev/null +++ b/dom/security/test/csp/file_block_all_mcb.sjs @@ -0,0 +1,78 @@ +// 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 0000000000..fdc1ae87ac --- /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 0000000000..4c4084e9ed --- /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_blocked_uri_in_violation_event_after_redirects.html b/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.html new file mode 100644 index 0000000000..74af0ff767 --- /dev/null +++ b/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1542194 - Check blockedURI in violation reports after redirects</title> + <meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' http://example.com"> +</head> +<body> +<button id="test1" onclick="createAndNavFrame('?test1a#ref1a')">Test 1: 302 redirect</button> +<button id="test2" onclick="createAndNavFrame('?test2a#ref2a')">Test 2: JS redirect</button> +<button id="test3" onclick="createAndNavFrame('?test3a#ref3a')">Test 3: Link navigation</button> +<div id="div"></div> +<script> + const SERVER_LOCATION = + "http://example.com/tests/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs"; + + document.addEventListener('securitypolicyviolation', e => { + // just forward the blockedURI to the parent + window.parent.postMessage({blockedURI: e.blockedURI}, '*'); + }); + + function createAndNavFrame(aTest) { + let myFrame = document.createElement('iframe'); + myFrame.src = SERVER_LOCATION + aTest; + div.appendChild(myFrame); + } + + window.onload = function() { + let button1 = document.getElementById("test1"); + button1.click(); + + let button2 = document.getElementById("test2"); + button2.click(); + + let button3 = document.getElementById("test3"); + button3.click(); + } +</script> +</body> +</html> diff --git a/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs b/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs new file mode 100644 index 0000000000..faf400d6d6 --- /dev/null +++ b/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs @@ -0,0 +1,52 @@ +// Redirect server specifically for the needs of Bug 1542194 + +"use strict"; + +let REDIRECT_302_URI = + "http://test1.example.com/tests/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs?test1b#ref1b"; + +let JS_REDIRECT = `<html> + <body> + <script> + var url= "http://test2.example.com/tests/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs?test2b#ref2b"; + window.location = url; + </script> + </body> + </html>`; + +let LINK_CLICK_NAVIGATION = `<html> + <body> + <a id="navlink" href="http://test3.example.com/tests/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs?test3b#ref3b">click me</a> + <script> + window.onload = function() { document.getElementById('navlink').click(); } + </script> + </body> + </html>`; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + let query = request.queryString; + + // Test 1: 302 redirect + if (query === "test1a") { + var newLocation = REDIRECT_302_URI; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", newLocation, false); + return; + } + + // Test 2: JS redirect + if (query === "test2a") { + response.setHeader("Content-Type", "text/html", false); + response.write(JS_REDIRECT); + return; + } + + // Test 3: Link navigation + if (query === "test3a") { + response.setHeader("Content-Type", "text/html", false); + response.write(LINK_CLICK_NAVIGATION); + return; + } +} diff --git a/dom/security/test/csp/file_blocked_uri_redirect_frame_src.html b/dom/security/test/csp/file_blocked_uri_redirect_frame_src.html new file mode 100644 index 0000000000..c3af4d5a09 --- /dev/null +++ b/dom/security/test/csp/file_blocked_uri_redirect_frame_src.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1687342 - Check blocked-uri in csp-reports after frame redirect</title> +</head> +<body> + Contents of the following iframe will be blocked<br/> + <iframe src="http://example.com/tests/dom/security/test/csp/file_blocked_uri_redirect_frame_src_server.sjs?doredirect#ref1"></iframe> +</body> +</html> diff --git a/dom/security/test/csp/file_blocked_uri_redirect_frame_src.html^headers^ b/dom/security/test/csp/file_blocked_uri_redirect_frame_src.html^headers^ new file mode 100644 index 0000000000..b69131f8eb --- /dev/null +++ b/dom/security/test/csp/file_blocked_uri_redirect_frame_src.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: frame-src http://example.com; report-uri http://mochi.test:8888/foo.sjs; diff --git a/dom/security/test/csp/file_blocked_uri_redirect_frame_src_server.sjs b/dom/security/test/csp/file_blocked_uri_redirect_frame_src_server.sjs new file mode 100644 index 0000000000..5e5dbdafdf --- /dev/null +++ b/dom/security/test/csp/file_blocked_uri_redirect_frame_src_server.sjs @@ -0,0 +1,14 @@ +// Redirect server specifically for the needs of Bug 1687342 + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + let query = request.queryString; + if (query === "doredirect") { + var newLocation = + "http://test1.example.com/tests/dom/security/test/csp/file_blocked_uri_redirect_frame_src_server.sjs?query#ref2"; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", newLocation, false); + return; + } +} diff --git a/dom/security/test/csp/file_bug1229639.html b/dom/security/test/csp/file_bug1229639.html new file mode 100644 index 0000000000..1e6152ead0 --- /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 0000000000..0177de7a38 --- /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 0000000000..18e0e5589e --- /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 0000000000..25a9483ea9 --- /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 0000000000..1b70c890fe --- /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_bug1452037.html b/dom/security/test/csp/file_bug1452037.html new file mode 100644 index 0000000000..0fb41d6654 --- /dev/null +++ b/dom/security/test/csp/file_bug1452037.html @@ -0,0 +1,9 @@ +<html> +<head> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content="script-src 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='"> +</head> +<body> + <a href="javascript:window.parent.postMessage({}, '*');">Click here</a> +</body> +</html> diff --git a/dom/security/test/csp/file_bug1505412.sjs b/dom/security/test/csp/file_bug1505412.sjs new file mode 100644 index 0000000000..dc7fa48b62 --- /dev/null +++ b/dom/security/test/csp/file_bug1505412.sjs @@ -0,0 +1,36 @@ +// 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) { + // 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://sub1.test1.example.org/tests/dom/security/test/csp/file_bug1505412.sjs?redirected"; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", loc, false); + return; + } + + // response.setHeader("content-type", "text/application", 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 = "info('Script Loaded')"; + + response.write(content); + + return; +} diff --git a/dom/security/test/csp/file_bug1505412_frame.html b/dom/security/test/csp/file_bug1505412_frame.html new file mode 100644 index 0000000000..b58af55849 --- /dev/null +++ b/dom/security/test/csp/file_bug1505412_frame.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title> Bug 1505412 CSP-RO reports violations in inline-scripts with nonce</title> + <script src="/tests/SimpleTest/SimpleTest.js" nonce="foobar"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + + +<body> + <script src="file_bug1505412.sjs" nonce="foobar"></script> +</body> + +</html> diff --git a/dom/security/test/csp/file_bug1505412_frame.html^headers^ b/dom/security/test/csp/file_bug1505412_frame.html^headers^ new file mode 100644 index 0000000000..e60b63c29c --- /dev/null +++ b/dom/security/test/csp/file_bug1505412_frame.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy-Report-Only: script-src 'nonce-foobar'; report-uri file_bug1505412_reporter.sjs diff --git a/dom/security/test/csp/file_bug1505412_reporter.sjs b/dom/security/test/csp/file_bug1505412_reporter.sjs new file mode 100644 index 0000000000..323a4edb1c --- /dev/null +++ b/dom/security/test/csp/file_bug1505412_reporter.sjs @@ -0,0 +1,18 @@ +function handleRequest(request, response) { + var receivedRequests = parseInt(getState("requests")); + if (isNaN(receivedRequests)) { + receivedRequests = 0; + } + if (request.queryString.includes("state")) { + response.write(receivedRequests); + return; + } + if (request.queryString.includes("flush")) { + setState("requests", "0"); + response.write("OK"); + return; + } + receivedRequests = receivedRequests + 1; + setState("requests", "" + receivedRequests); + response.write("OK"); +} diff --git a/dom/security/test/csp/file_bug1738418_child.html b/dom/security/test/csp/file_bug1738418_child.html new file mode 100644 index 0000000000..26e7f8f1f6 --- /dev/null +++ b/dom/security/test/csp/file_bug1738418_child.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<body> +<script type="text/javascript"> + window.parent.parent.postMessage({ + element: location.hash.substr(1), + domain: document.domain, + }, '*'); +</script> +</body> +</html> diff --git a/dom/security/test/csp/file_bug1738418_parent.html b/dom/security/test/csp/file_bug1738418_parent.html new file mode 100644 index 0000000000..c8bdbb2c46 --- /dev/null +++ b/dom/security/test/csp/file_bug1738418_parent.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> + <base href="file_bug1738418_child.html"> +</head> +<body> + <iframe src="#iframe"></iframe> + <embed src="#embed"></embed> + <object data="#object"></object> +</body> +</html> diff --git a/dom/security/test/csp/file_bug1738418_parent.html^headers^ b/dom/security/test/csp/file_bug1738418_parent.html^headers^ new file mode 100644 index 0000000000..4705ce9ded --- /dev/null +++ b/dom/security/test/csp/file_bug1738418_parent.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: sandbox allow-scripts; diff --git a/dom/security/test/csp/file_bug1777572.html b/dom/security/test/csp/file_bug1777572.html new file mode 100644 index 0000000000..51f2a80d28 --- /dev/null +++ b/dom/security/test/csp/file_bug1777572.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<html> +<head> + <meta http-equiv="Content-Security-Policy" content="img-src https://*;"> + <script> + async function timeout (cmd) { + const timer = new Promise((resolve, reject) => { + const id = setTimeout(() => { + clearTimeout(id) + reject(new Error('Promise timed out!')) + }, 750) + }) + return Promise.race([cmd, timer]) + } + + let ourOpener = window.opener; + + if (location.search.includes("close")) { + window.close(); + } + + document.addEventListener('DOMContentLoaded', async () => { + const frame = document.createElementNS('http://www.w3.org/1999/xhtml', 'frame'); + const image = document.createElementNS('http://www.w3.org/2000/svg', 'image'); + document.documentElement.appendChild(frame) + image.setAttribute('href', 'a.png') + for (let i = 0; i < 5; ++i) { + try { await timeout(image.decode()) } catch (e) {} + } + let w = window.open(); + // Need to run SpecialPowers in the newly opened window to avoid + // .wrap throwing because of dead objects. + let csp = w.eval("SpecialPowers.wrap(document).cspJSON;"); + ourOpener.postMessage(csp, "*"); + w.close(); + + if (!location.search.includes("close")) { + window.close(); + } + }) + </script> +</head> +</html> diff --git a/dom/security/test/csp/file_bug663567.xsl b/dom/security/test/csp/file_bug663567.xsl new file mode 100644 index 0000000000..b12b0d3b1d --- /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 0000000000..93d3451038 --- /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 0000000000..4c6fa3c26a --- /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 0000000000..93d3451038 --- /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 0000000000..baf7f3c6af --- /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 0000000000..dae040376b --- /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 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 0000000000..4c6fa3c26a --- /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 0000000000..1e47b643a6 --- /dev/null +++ b/dom/security/test/csp/file_bug802872.js @@ -0,0 +1,47 @@ +/* + * 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 allowlisted 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 0000000000..6877bd5833 --- /dev/null +++ b/dom/security/test/csp/file_bug802872.sjs @@ -0,0 +1,6 @@ +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 0000000000..6a728813a7 --- /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 0000000000..ec6ba8c4ae --- /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 0000000000..f465f09f22 --- /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 0000000000..5c8318c252 --- /dev/null +++ b/dom/security/test/csp/file_bug836922_npolicies_violation.sjs @@ -0,0 +1,64 @@ +// 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 0000000000..5d7aacbda4 --- /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 0000000000..767b9ca926 --- /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 0000000000..2279b33e46 --- /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 0000000000..f82598b673 --- /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 0000000000..ec8c9e7e92 --- /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 0000000000..4c6fa3c26a --- /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 0000000000..83d36c55ae --- /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 0000000000..4c6fa3c26a --- /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 0000000000..8b4313000f --- /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 0000000000..6581fd425e --- /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 0000000000..41137ea017 --- /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 0000000000..6581fd425e --- /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 0000000000..82c10f20c0 --- /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}, "*"); + } + + 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 0000000000..3abc190552 --- /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 0000000000..f6567b470e --- /dev/null +++ b/dom/security/test/csp/file_bug886164_6.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.postMessage({ok: result, 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 0000000000..6f9fc3f25d --- /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 0000000000..27cf9b00ab --- /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 0000000000..de8cf96811 --- /dev/null +++ b/dom/security/test/csp/file_bug888172.sjs @@ -0,0 +1,47 @@ +// SJS file for CSP mochitests + +const { NetUtil } = ChromeUtils.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.nsIFile); + 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 0000000000..0d4934a4a3 --- /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 0000000000..ecb3458750 --- /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 0000000000..bcb907a965 --- /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 0000000000..eccc1c0110 --- /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 0000000000..56647134f8 --- /dev/null +++ b/dom/security/test/csp/file_bug910139.sjs @@ -0,0 +1,54 @@ +// Server side js file for bug 910139, see file test_bug910139.html for details. + +const { NetUtil } = ChromeUtils.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.nsIFile); + 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 0000000000..29feba9418 --- /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 0000000000..b99abca099 --- /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 0000000000..3a2e636e0b --- /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 0000000000..22e176f208 --- /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 0000000000..1e5f70cc37 --- /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 0000000000..18749011b9 --- /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); + + 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); + testframe.addEventListener('error', reportError); + + 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 0000000000..f0c4e66fa0 --- /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); + </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 0000000000..b291a4a4e8 --- /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 0000000000..68cd8401f8 --- /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 0000000000..313915302e --- /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 0000000000..ce0c0261ed --- /dev/null +++ b/dom/security/test/csp/file_child-src_shared_worker.html @@ -0,0 +1,35 @@ +<!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") + + "&type=text/javascript", + 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 0000000000..98702469c5 --- /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 0000000000..a4befe4ca3 --- /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 0000000000..b0029935c2 --- /dev/null +++ b/dom/security/test/csp/file_child-src_worker-redirect.html @@ -0,0 +1,47 @@ +<!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) { + // this means CSP blocked it + var msg = !("message" in error) ? "blocked" : e.message; + 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 0000000000..a9fdbb3282 --- /dev/null +++ b/dom/security/test/csp/file_child-src_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 Worker('file_testserver.sjs?file='+ + escape("tests/dom/security/test/csp/file_child-src_worker.js") + +"&type=text/javascript"); + + 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 0000000000..5a615a98a8 --- /dev/null +++ b/dom/security/test/csp/file_child-src_worker.js @@ -0,0 +1,3 @@ +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 0000000000..e9e22f01da --- /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_connect-src-fetch.html b/dom/security/test/csp/file_connect-src-fetch.html new file mode 100644 index 0000000000..ff9b2f740b --- /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 0000000000..17a940a0e0 --- /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_csp_frame_ancestors_about_blank.html b/dom/security/test/csp/file_csp_frame_ancestors_about_blank.html new file mode 100644 index 0000000000..6ce361a438 --- /dev/null +++ b/dom/security/test/csp/file_csp_frame_ancestors_about_blank.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Helper file for Bug 1668071 - CSP frame-ancestors in about:blank</title> +</head> +<body> + CSP frame-ancestors in about:blank +</body> +</html> diff --git a/dom/security/test/csp/file_csp_frame_ancestors_about_blank.html^headers^ b/dom/security/test/csp/file_csp_frame_ancestors_about_blank.html^headers^ new file mode 100644 index 0000000000..e5d129c3e8 --- /dev/null +++ b/dom/security/test/csp/file_csp_frame_ancestors_about_blank.html^headers^ @@ -0,0 +1,2 @@ +Cache-Control: no-cache +Content-Security-Policy: frame-ancestors http://mochi.test:8888 http://mochi.xorigin-test:8888 diff --git a/dom/security/test/csp/file_csp_meta_uir.html b/dom/security/test/csp/file_csp_meta_uir.html new file mode 100644 index 0000000000..dba1030975 --- /dev/null +++ b/dom/security/test/csp/file_csp_meta_uir.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title>Hello World</title> + <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> +</head> +<body> + <script> + document.write("<a href='" + document.location.href.replace(/^https/, "http") + "' id='mylink'>Click me</a>"); + </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 0000000000..59b7b25902 --- /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 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 0000000000..4248cca188 --- /dev/null +++ b/dom/security/test/csp/file_data-uri_blocked.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' 'report-sample'; img-src 'none' 'report-sample' diff --git a/dom/security/test/csp/file_data_csp_inheritance.html b/dom/security/test/csp/file_data_csp_inheritance.html new file mode 100644 index 0000000000..4ae2fedc69 --- /dev/null +++ b/dom/security/test/csp/file_data_csp_inheritance.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1381761 - Treating 'data:' documents as unique, opaque origins should still inherit the CSP</title> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content= "img-src 'none'"/> +</head> +<body> +<iframe id="dataFrame" src="data:text/html,<body>should inherit csp</body>"></iframe> + +<script type="application/javascript"> + // get the csp in JSON notation from the principal + var frame = document.getElementById("dataFrame"); + frame.onload = function () { + var contentDoc = SpecialPowers.wrap(frame).contentDocument; + var cspOBJ = JSON.parse(contentDoc.cspJSON); + // make sure we got >>one<< policy + var policies = cspOBJ["csp-policies"]; + window.parent.postMessage({result: policies.length}, "*"); + } +</script> + +</body> +</html> diff --git a/dom/security/test/csp/file_data_csp_merge.html b/dom/security/test/csp/file_data_csp_merge.html new file mode 100644 index 0000000000..88ae8febe5 --- /dev/null +++ b/dom/security/test/csp/file_data_csp_merge.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1386183 - Meta CSP on data: URI iframe should be merged with toplevel CSP</title> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content= "img-src https:"/> +</head> +<body> +<iframe id="dataFrame" onload="doCSPMergeCheck()" + src="data:text/html,<html><head><meta http-equiv='Content-Security-Policy' content='script-src https:'/></head><body>merge csp</body></html>"> +</iframe> + +<script type="application/javascript"> + function doCSPMergeCheck() { + // get the csp in JSON notation from the principal + var frame = document.getElementById("dataFrame"); + var contentDoc = SpecialPowers.wrap(frame).contentDocument; + var cspOBJ = JSON.parse(contentDoc.cspJSON); + // make sure we got >>two<< policies + var policies = cspOBJ["csp-policies"]; + window.parent.postMessage({result: policies.length}, "*"); + } +</script> + +</body> +</html> diff --git a/dom/security/test/csp/file_data_doc_ignore_meta_csp.html b/dom/security/test/csp/file_data_doc_ignore_meta_csp.html new file mode 100644 index 0000000000..9d6e9834dd --- /dev/null +++ b/dom/security/test/csp/file_data_doc_ignore_meta_csp.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1382869: data document should ignore meta csp</title> + <meta charset="utf-8"> +</head> +<body> +<script type="application/javascript"> + // 1) create a data document + const doc = document.implementation.createHTMLDocument(); + // 2) add meta csp to that document + const metaEl = doc.createElement('meta'); + metaEl.setAttribute('http-equiv', 'Content-Security-Policy'); + metaEl.setAttribute('content', "img-src 'none'"); + doc.head.appendChild(metaEl); + // 3) let the parent know we are done here + var result = "dataDocCreated"; + window.parent.postMessage({result}, "*"); +</script> + +</body> +</html> 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 0000000000..a0f36a4bfe --- /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 0000000000..de725038b6 --- /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 0000000000..292de3bec5 --- /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 0000000000..722adc235e --- /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 0000000000..0efe186d57 --- /dev/null +++ b/dom/security/test/csp/file_dual_header_testserver.sjs @@ -0,0 +1,45 @@ +/* + * 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_dummy_pixel.png b/dom/security/test/csp/file_dummy_pixel.png Binary files differnew file mode 100644 index 0000000000..52c591798e --- /dev/null +++ b/dom/security/test/csp/file_dummy_pixel.png diff --git a/dom/security/test/csp/file_empty_directive.html b/dom/security/test/csp/file_empty_directive.html new file mode 100644 index 0000000000..16196bb19f --- /dev/null +++ b/dom/security/test/csp/file_empty_directive.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_empty_directive.html^headers^ b/dom/security/test/csp/file_empty_directive.html^headers^ new file mode 100644 index 0000000000..50dbe57bb9 --- /dev/null +++ b/dom/security/test/csp/file_empty_directive.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: ; 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 0000000000..e83c1d9ed7 --- /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 0000000000..b91ba384d9 --- /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 0000000000..23c385afe4 --- /dev/null +++ b/dom/security/test/csp/file_evalscript_main.js @@ -0,0 +1,240 @@ +// 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." + ); + } + + // ShadowRealm.prototype.evaluate -- should throw exception as per spec. + try { + var sr = new ShadowRealm(); + sr.evaluate("var x = 10"); + onevalexecuted( + false, + "ShadowRealm.prototype.evaluate(String)", + "ShadowRealm.prototype.evaluate(String) was enabled." + ); + } catch (e) { + onevalblocked( + false, + "ShadowRealm.prototype.evaluate(String)", + "ShadowRealm.prototype.evaluate(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 0000000000..274972d9bd --- /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 0000000000..0cb5288bec --- /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 0000000000..ce180f64e8 --- /dev/null +++ b/dom/security/test/csp/file_evalscript_main_allowed.js @@ -0,0 +1,193 @@ +// 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." + ); + } + + // ShadowRealm.prototype.evaluate + try { + var sr = new ShadowRealm(); + sr.evaluate("var x = 10"); + onevalexecuted( + true, + "ShadowRealm.prototype.evaluate(String)", + "ShadowRealm.prototype.evaluate(String) was enabled." + ); + } catch (e) { + onevalblocked( + true, + "ShadowRealm.prototype.evaluate(String)", + "ShadowRealm.prototype.evaluate(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 0000000000..b9b5e602fe --- /dev/null +++ b/dom/security/test/csp/file_fontloader.sjs @@ -0,0 +1,57 @@ +// 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 0000000000..fbf7390d59 --- /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 0000000000..cfff156bae --- /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 0000000000..0c79736d47 --- /dev/null +++ b/dom/security/test/csp/file_form_action_server.sjs @@ -0,0 +1,32 @@ +// 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_frame_ancestors_ro.html b/dom/security/test/csp/file_frame_ancestors_ro.html new file mode 100644 index 0000000000..ff5ae9cf9f --- /dev/null +++ b/dom/security/test/csp/file_frame_ancestors_ro.html @@ -0,0 +1 @@ +<html><body>Child Document</body></html> diff --git a/dom/security/test/csp/file_frame_ancestors_ro.html^headers^ b/dom/security/test/csp/file_frame_ancestors_ro.html^headers^ new file mode 100644 index 0000000000..d018af3a96 --- /dev/null +++ b/dom/security/test/csp/file_frame_ancestors_ro.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy-Report-Only: frame-ancestors 'none'; report-uri http://mochi.test:8888/foo.sjs diff --git a/dom/security/test/csp/file_frame_src.js b/dom/security/test/csp/file_frame_src.js new file mode 100644 index 0000000000..db9ac0ecbf --- /dev/null +++ b/dom/security/test/csp/file_frame_src.js @@ -0,0 +1,20 @@ +let testframe = document.getElementById("testframe"); +testframe.onload = function() { + parent.postMessage( + { + result: "frame-allowed", + href: document.location.href, + }, + "*" + ); +}; +testframe.onerror = function() { + parent.postMessage( + { + result: "frame-blocked", + href: document.location.href, + }, + "*" + ); +}; +testframe.src = "file_frame_src_inner.html"; diff --git a/dom/security/test/csp/file_frame_src_child_governs.html b/dom/security/test/csp/file_frame_src_child_governs.html new file mode 100644 index 0000000000..a51cb75be2 --- /dev/null +++ b/dom/security/test/csp/file_frame_src_child_governs.html @@ -0,0 +1,10 @@ +<html> +<head> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content="child-src https://example.com">"; +</head> +<body> +<iframe id="testframe"></iframe> +<script type="text/javascript" src="file_frame_src.js"></script> +</body> +</html> diff --git a/dom/security/test/csp/file_frame_src_frame_governs.html b/dom/security/test/csp/file_frame_src_frame_governs.html new file mode 100644 index 0000000000..2c5d5857f2 --- /dev/null +++ b/dom/security/test/csp/file_frame_src_frame_governs.html @@ -0,0 +1,10 @@ +<html> +<head> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content="frame-src https://example.com; child-src 'none'">"; +</head> +<body> +<iframe id="testframe"></iframe> +<script type="text/javascript" src="file_frame_src.js"></script> +</body> +</html> diff --git a/dom/security/test/csp/file_frame_src_inner.html b/dom/security/test/csp/file_frame_src_inner.html new file mode 100644 index 0000000000..4a2fc6095a --- /dev/null +++ b/dom/security/test/csp/file_frame_src_inner.html @@ -0,0 +1,5 @@ +<html> +<body> +dummy iframe +</body> +</html> diff --git a/dom/security/test/csp/file_frameancestors.sjs b/dom/security/test/csp/file_frameancestors.sjs new file mode 100644 index 0000000000..6627c8b027 --- /dev/null +++ b/dom/security/test/csp/file_frameancestors.sjs @@ -0,0 +1,69 @@ +// 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 0000000000..97f9cb9ac5 --- /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 0000000000..ee251020d2 --- /dev/null +++ b/dom/security/test/csp/file_frameancestors_main.js @@ -0,0 +1,135 @@ +// 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", + }; + + // In both cases (base.a, base.b) the path starts with /tests/. Let's make sure this + // path within the CSP policy is completely ignored when enforcing frame ancestors. + // To test this behavior we use /foo/ and /bar/ as dummy values for the path. + var host = { + a: "http://mochi.test:8888/foo/", + b: "http://example.com:80/bar/", + }; + + 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); diff --git a/dom/security/test/csp/file_frameancestors_userpass.html b/dom/security/test/csp/file_frameancestors_userpass.html new file mode 100644 index 0000000000..c840995b6c --- /dev/null +++ b/dom/security/test/csp/file_frameancestors_userpass.html @@ -0,0 +1,10 @@ +<html> +<head> + <title>CSP frame ancestors tests</title> +</head> +<body> + <tt>Nested Frames</tt><br/> + <iframe src='http://sampleuser:samplepass@mochi.test:8888/tests/dom/security/test/csp/file_frameancestors_userpass_frame_a.html'></iframe><br/> + <iframe src='http://sampleuser:samplepass@example.com/tests/dom/security/test/csp/file_frameancestors_userpass_frame_b.html'></iframe><br/> +</body> +</html> diff --git a/dom/security/test/csp/file_frameancestors_userpass_frame_a.html b/dom/security/test/csp/file_frameancestors_userpass_frame_a.html new file mode 100644 index 0000000000..d5a5bb604b --- /dev/null +++ b/dom/security/test/csp/file_frameancestors_userpass_frame_a.html @@ -0,0 +1,12 @@ +<html> +<head> + <title>Nested frame</title> + <script> + parent.parent.postMessage({call: "frameLoaded", testname: "frame_a", uri: window.location.toString()}, "*"); + </script> +</head> +<body> + <tt>IFRAME A</tt><br/> + <iframe src='http://sampleuser:samplepass@mochi.test:8888/tests/dom/security/test/csp/file_frameancestors_userpass_frame_c.html'></iframe><br/> +</body> +</html> diff --git a/dom/security/test/csp/file_frameancestors_userpass_frame_b.html b/dom/security/test/csp/file_frameancestors_userpass_frame_b.html new file mode 100644 index 0000000000..87055ef149 --- /dev/null +++ b/dom/security/test/csp/file_frameancestors_userpass_frame_b.html @@ -0,0 +1,12 @@ +<html> +<head> + <title>Nested frame</title> + <script> + parent.parent.postMessage({call: "frameLoaded", testname: "frame_b", uri: window.location.toString()}, "*"); + </script> +</head> +<body> + <tt>IFRAME B</tt><br/> + <iframe src='http://sampleuser:samplepass@example.com/tests/dom/security/test/csp/file_frameancestors_userpass_frame_d.html'></iframe><br/> +</body> +</html> diff --git a/dom/security/test/csp/file_frameancestors_userpass_frame_c.html b/dom/security/test/csp/file_frameancestors_userpass_frame_c.html new file mode 100644 index 0000000000..159e6c4633 --- /dev/null +++ b/dom/security/test/csp/file_frameancestors_userpass_frame_c.html @@ -0,0 +1,8 @@ +<html> +<head> + <title>Nested frame</title> +</head> +<body> + Nested frame C content +</body> +</html> diff --git a/dom/security/test/csp/file_frameancestors_userpass_frame_c.html^headers^ b/dom/security/test/csp/file_frameancestors_userpass_frame_c.html^headers^ new file mode 100644 index 0000000000..9e7dfefcda --- /dev/null +++ b/dom/security/test/csp/file_frameancestors_userpass_frame_c.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'none'; frame-ancestors http://mochi.test:8888/ ; script-src 'self'; diff --git a/dom/security/test/csp/file_frameancestors_userpass_frame_d.html b/dom/security/test/csp/file_frameancestors_userpass_frame_d.html new file mode 100644 index 0000000000..0cb49c4836 --- /dev/null +++ b/dom/security/test/csp/file_frameancestors_userpass_frame_d.html @@ -0,0 +1,8 @@ +<html> +<head> + <title>Nested frame</title> +</head> +<body> + Nested frame D content +</body> +</html> diff --git a/dom/security/test/csp/file_frameancestors_userpass_frame_d.html^headers^ b/dom/security/test/csp/file_frameancestors_userpass_frame_d.html^headers^ new file mode 100644 index 0000000000..019fcea026 --- /dev/null +++ b/dom/security/test/csp/file_frameancestors_userpass_frame_d.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'none'; frame-ancestors http://sampleuser:samplepass@example.com/ ; script-src 'self'; 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 0000000000..47eba6cf3e --- /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 0000000000..785d63391e --- /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_parent_location_js.html b/dom/security/test/csp/file_iframe_parent_location_js.html new file mode 100644 index 0000000000..0d980f9925 --- /dev/null +++ b/dom/security/test/csp/file_iframe_parent_location_js.html @@ -0,0 +1,10 @@ +<html> +<head> + <title>Test setting parent location to javascript:</title> +</head> +<body> +<script> + parent.window.location ="javascript:location.href"; +</script> +</body> +</html> 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 0000000000..a3a0952941 --- /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}, "*"); + } + 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 0000000000..bc700ed68f --- /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 0000000000..cf869e07d4 --- /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 0000000000..9f8774e4cc --- /dev/null +++ b/dom/security/test/csp/file_iframe_srcdoc.sjs @@ -0,0 +1,87 @@ +// 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 0000000000..773184201c --- /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 allowlisted by 'unsafe-inline' --> +<script type="application/javascript"> +document.getElementById('testdiv').innerHTML += 'b'; +</script> + +<!-- second script allowlisted by hash --> +<!-- sha256-uJXAPKP5NZxnVMZMUkDofh6a9P3UMRc1CRTevVPS/rI= --> +<script type="application/javascript"> +document.getElementById('testdiv').innerHTML += 'c'; +</script> + +<!-- thrid script allowlisted 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 0000000000..40301b3cc3 --- /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 + +const { NetUtil } = ChromeUtils.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.nsIFile); + 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_ignore_xfo.html b/dom/security/test/csp/file_ignore_xfo.html new file mode 100644 index 0000000000..6746a3adba --- /dev/null +++ b/dom/security/test/csp/file_ignore_xfo.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1024557: Ignore x-frame-options if CSP with frame-ancestors exists</title> +</head> +<body> +<div id="cspmessage">Ignoring XFO because of CSP</div> +</body> +</html> diff --git a/dom/security/test/csp/file_ignore_xfo.html^headers^ b/dom/security/test/csp/file_ignore_xfo.html^headers^ new file mode 100644 index 0000000000..e93f9e3ecb --- /dev/null +++ b/dom/security/test/csp/file_ignore_xfo.html^headers^ @@ -0,0 +1,3 @@ +Content-Security-Policy: frame-ancestors http://mochi.test:8888 +X-Frame-Options: deny +Cache-Control: no-cache diff --git a/dom/security/test/csp/file_image_document_pixel.png b/dom/security/test/csp/file_image_document_pixel.png Binary files differnew file mode 100644 index 0000000000..52c591798e --- /dev/null +++ b/dom/security/test/csp/file_image_document_pixel.png diff --git a/dom/security/test/csp/file_image_document_pixel.png^headers^ b/dom/security/test/csp/file_image_document_pixel.png^headers^ new file mode 100644 index 0000000000..7c727854d0 --- /dev/null +++ b/dom/security/test/csp/file_image_document_pixel.png^headers^ @@ -0,0 +1,2 @@ +Content-Security-Policy: default-src https://bug1627235.test.com +Cache-Control: no-cache diff --git a/dom/security/test/csp/file_image_nonce.html b/dom/security/test/csp/file_image_nonce.html new file mode 100644 index 0000000000..5d57bb8372 --- /dev/null +++ b/dom/security/test/csp/file_image_nonce.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'> + <title>Bug 1355801: Nonce should not apply to images</title> + </head> +<body> + +<img id='matchingNonce' src='http://mochi.test:8888/tests/image/test/mochitest/blue.png?a' nonce='abc'></img> +<img id='nonMatchingNonce' src='http://mochi.test:8888/tests/image/test/mochitest/blue.png?b' nonce='bca'></img> +<img id='noNonce' src='http://mochi.test:8888/tests/image/test/mochitest/blue.png?c'></img> + +<script type='application/javascript'> + var matchingNonce = document.getElementById('matchingNonce'); + matchingNonce.onload = function(e) { + window.parent.postMessage({result: 'img-with-matching-nonce-loaded'}, '*'); + }; + matchingNonce.onerror = function(e) { + window.parent.postMessage({result: 'img-with-matching-nonce-blocked'}, '*'); + } + + var nonMatchingNonce = document.getElementById('nonMatchingNonce'); + nonMatchingNonce.onload = function(e) { + window.parent.postMessage({result: 'img-with_non-matching-nonce-loaded'}, '*'); + }; + nonMatchingNonce.onerror = function(e) { + window.parent.postMessage({result: 'img-with_non-matching-nonce-blocked'}, '*'); + } + + var noNonce = document.getElementById('noNonce'); + noNonce.onload = function(e) { + window.parent.postMessage({result: 'img-without-nonce-loaded'}, '*'); + }; + noNonce.onerror = function(e) { + window.parent.postMessage({result: 'img-without-nonce-blocked'}, '*'); + } +</script> +</body> +</html> diff --git a/dom/security/test/csp/file_image_nonce.html^headers^ b/dom/security/test/csp/file_image_nonce.html^headers^ new file mode 100644 index 0000000000..0d63558c46 --- /dev/null +++ b/dom/security/test/csp/file_image_nonce.html^headers^ @@ -0,0 +1,2 @@ +Content-Security-Policy: img-src 'nonce-abc'; +Cache-Control: no-cache diff --git a/dom/security/test/csp/file_independent_iframe_csp.html b/dom/security/test/csp/file_independent_iframe_csp.html new file mode 100644 index 0000000000..0581f5ea85 --- /dev/null +++ b/dom/security/test/csp/file_independent_iframe_csp.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1419222 - iFrame CSP should not affect parent document CSP</title> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content="connect-src *; style-src * 'unsafe-inline'; "/> +</head> +<body> + <script> + var getCspObj = function(doc) { + var contentDoc = SpecialPowers.wrap(doc); + var cspJSON = contentDoc.cspJSON; + var cspOBJ = JSON.parse(cspJSON); + return cspOBJ; + } + + // Add an iFrame, add an additional CSP directive to that iFrame, and + // return the CSP object of that iFrame. + var addIFrame = function() { + var frame = document.createElement("iframe"); + frame.id = "nestedframe"; + document.body.appendChild(frame); + var metaTag = document.createElement("meta"); + metaTag.setAttribute("http-equiv", "Content-Security-Policy"); + metaTag.setAttribute("content", "img-src 'self' data:;"); + frame.contentDocument.head.appendChild(metaTag); + return getCspObj(frame.contentDocument); + } + + // Get the CSP objects of the parent document before and after adding the + // iFrame, as well as of the iFram itself. + var parentBeginCspObj = getCspObj(document); + var iFrameCspObj = addIFrame(); + var parentEndCspObj = getCspObj(document); + + // Post a message containing the three CSP objects to the test context. + window.parent.postMessage( + {result: [parentBeginCspObj, iFrameCspObj, parentEndCspObj]}, + "*" + ); + </script> +</body> +</html> diff --git a/dom/security/test/csp/file_inlinescript.html b/dom/security/test/csp/file_inlinescript.html new file mode 100644 index 0000000000..55a9b9b180 --- /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 0000000000..a0d2969883 --- /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 0000000000..7b6a251679 --- /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 0000000000..9b533ef074 --- /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 0000000000..621d2536b0 --- /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 0000000000..83bb0ec0ca --- /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 0000000000..ea5e993447 --- /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_link_rel_preload.html b/dom/security/test/csp/file_link_rel_preload.html new file mode 100644 index 0000000000..8af49a77fe --- /dev/null +++ b/dom/security/test/csp/file_link_rel_preload.html @@ -0,0 +1,19 @@ +<!doctype html> +<html> +<head> + <title>Bug 1599791 - Test link rel=preload</title> + <!-- Please note that fakeServer does not exist in our testsuite --> + <meta http-equiv="Content-Security-Policy" content="default-src 'none'"> + <link rel="preload" as="script" href="fakeServer?script"></link> + <link rel="preload" as="style" href="fakeServer?style"></link> + <link rel="preload" as="image" href="fakeServer?image"></link> + <link rel="preload" as="fetch" href="fakeServer?fetch"></link> + <link rel="preload" as="font" href="fakeServer?font"></link> + + <link rel="stylesheet" href="fakeServer?style"> +</head> +<body> +<script src="fakeServer?script"></script> +<img src="fakeServer?image"></img> +</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 0000000000..ddc8382617 --- /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 0000000000..3338de389b --- /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 0000000000..01dd43cbf5 --- /dev/null +++ b/dom/security/test/csp/file_main.js @@ -0,0 +1,26 @@ +function doXHR(uri) { + try { + var xhr = new XMLHttpRequest(); + xhr.open("GET", uri); + xhr.send(); + } 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" +); 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 0000000000..17f19c7c86 --- /dev/null +++ b/dom/security/test/csp/file_meta_element.html @@ -0,0 +1,27 @@ +<!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"></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"}, "*"); + }; + //Image should be tried to load only after onload/onerror event declaration. + myImg.src = "http://mochi.test:8888/tests/image/test/mochitest/blue.png"; + </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 0000000000..445b3e444e --- /dev/null +++ b/dom/security/test/csp/file_meta_header_dual.sjs @@ -0,0 +1,101 @@ +// 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 0000000000..c0cfc8cc28 --- /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 0000000000..a3cb415a9e --- /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 0000000000..e1b64a9220 --- /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 0000000000..3fa6c7ab91 --- /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 0000000000..b523073cd3 --- /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 0000000000..f59f011ce7 --- /dev/null +++ b/dom/security/test/csp/file_multipart_testserver.sjs @@ -0,0 +1,160 @@ +// SJS file specifically for the needs of bug +// Bug 1416045/Bug 1223743 - CSP: Check baseChannel for CSP when loading multipart channel + +var CSP = "script-src 'unsafe-inline', img-src 'none'"; +var rootCSP = "script-src 'unsafe-inline'"; +var part1CSP = "img-src *"; +var part2CSP = "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({"test": "rootCSP_test", + "msg": "img-blocked"}, "*"); + }; + myImg.onload = function() { + window.parent.postMessage({"test": "rootCSP_test", + "msg": "img-loaded"}, "*"); + }; + document.body.appendChild(myImg); + </script> +`; + +var RESPONSE1 = ` + <body> + <script> + var triggerNextPartFrame = document.createElement('iframe'); + var myImg = new Image; + myImg.src = "file_multipart_testserver.sjs?img"; + myImg.onerror = function(e) { + window.parent.postMessage({"test": "part1CSP_test", + "msg": "part1-img-blocked"}, "*"); + triggerNextPartFrame.src = 'file_multipart_testserver.sjs?sendnextpart'; + }; + myImg.onload = function() { + window.parent.postMessage({"test": "part1CSP_test", + "msg": "part1-img-loaded"}, "*"); + triggerNextPartFrame.src = 'file_multipart_testserver.sjs?sendnextpart'; + }; + document.body.appendChild(myImg); + document.body.appendChild(triggerNextPartFrame); + </script> + </body> +`; + +var RESPONSE2 = ` + <body> + <script> + var myImg = new Image; + myImg.src = "file_multipart_testserver.sjs?img"; + myImg.onerror = function(e) { + window.parent.postMessage({"test": "part2CSP_test", + "msg": "part2-img-blocked"}, "*"); + }; + myImg.onload = function() { + window.parent.postMessage({"test": "part2CSP_test", + "msg": "part2-img-loaded"}, "*"); + }; + document.body.appendChild(myImg); + </script> + </body> +`; + +function setGlobalState(data, key) { + x = { + data, + QueryInterface(iid) { + return this; + }, + }; + x.wrappedJSObject = x; + setObjectState(key, x); +} + +function getGlobalState(key) { + var data; + getObjectState(key, function(x) { + data = x && x.wrappedJSObject.data; + }); + return data; +} + +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 == "partcspdoc") { + response.setHeader("Content-Security-Policy", rootCSP, false); + response.setHeader( + "Content-Type", + "multipart/x-mixed-replace; boundary=" + BOUNDARY, + false + ); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.processAsync(); + response.write("--" + BOUNDARY + "\r\n"); + sendNextPart(response, 1); + return; + } + + if (request.queryString == "sendnextpart") { + response.setStatusLine(request.httpVersion, 204, "No content"); + var blockedResponse = getGlobalState("root-document-response"); + if (typeof blockedResponse == "object") { + sendNextPart(blockedResponse, 2); + sendClose(blockedResponse); + } else { + dump("Couldn't find the stored response object."); + } + 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"); +} + +function sendClose(response) { + response.write("--" + BOUNDARY + "--\r\n"); + response.finish(); +} + +function sendNextPart(response, partNumber) { + response.write("Content-type: text/html" + "\r\n"); + if (partNumber == 1) { + response.write("Content-Security-Policy:" + part1CSP + "\r\n"); + response.write(RESPONSE1); + setGlobalState(response, "root-document-response"); + } else { + response.write("Content-Security-Policy:" + part2CSP + "\r\n"); + response.write(RESPONSE2); + } + response.write("--" + BOUNDARY + "\r\n"); +} diff --git a/dom/security/test/csp/file_navigate_to.html b/dom/security/test/csp/file_navigate_to.html new file mode 100644 index 0000000000..f6ea36d389 --- /dev/null +++ b/dom/security/test/csp/file_navigate_to.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1529068 Implement CSP 'navigate-to' directive</title> +</head> +<body> + <script type="text/javascript"> + window.location = "http://www.example.com/"; + </script> +</body> +</html> diff --git a/dom/security/test/csp/file_navigate_to.sjs b/dom/security/test/csp/file_navigate_to.sjs new file mode 100644 index 0000000000..d1cffb74cc --- /dev/null +++ b/dom/security/test/csp/file_navigate_to.sjs @@ -0,0 +1,58 @@ +// Custom *.sjs file specifically for the needs of +// https://bugzilla.mozilla.org/show_bug.cgi?id=1529068 + +"use strict"; +Components.utils.importGlobalProperties(["URLSearchParams"]); + +const TEST_NAVIGATION_HEAD = ` + <!DOCTYPE HTML> + <html> + <head> + <title>Bug 1529068 Implement CSP 'navigate-to' directive</title>`; + +const TEST_NAVIGATION_AFTER_META = ` + </head> + <body> + <script type="text/javascript"> + window.location = "`; + +const TEST_NAVIGATION_FOOT = `"; + </script> + </body> + </html> + `; + +function handleRequest(request, response) { + const query = new URLSearchParams(request.queryString); + + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + if (query.get("redir")) { + response.setStatusLine(request.httpVersion, "302", "Found"); + response.setHeader("Location", query.get("redir"), false); + return; + } + + response.write(TEST_NAVIGATION_HEAD); + + // We need meta to set multiple CSP headers. + if (query.get("csp")) { + response.write( + '<meta http-equiv="Content-Security-Policy" content="' + + query.get("csp") + + '">' + ); + } + if (query.get("csp2")) { + response.write( + '<meta http-equiv="Content-Security-Policy" content="' + + query.get("csp2") + + '">' + ); + } + + response.write( + TEST_NAVIGATION_AFTER_META + query.get("target") + TEST_NAVIGATION_FOOT + ); +} diff --git a/dom/security/test/csp/file_navigate_to_request.html b/dom/security/test/csp/file_navigate_to_request.html new file mode 100644 index 0000000000..4f82525599 --- /dev/null +++ b/dom/security/test/csp/file_navigate_to_request.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> +</head> +<script type="text/javascript"> + // The idea with this file is to convert responses into requests. + // This is needed because we don't have + // specialpowers-http-notify-response + + // Response from this file => request to www.example.com => Allowed + // CSP error => Blocked + fetch('http://www.example.com/'); +</script> +<body> +</body> +</html> diff --git a/dom/security/test/csp/file_no_log_ignore_xfo.html b/dom/security/test/csp/file_no_log_ignore_xfo.html new file mode 100644 index 0000000000..414c53deb9 --- /dev/null +++ b/dom/security/test/csp/file_no_log_ignore_xfo.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1722252: "Content Security Policy: Ignoring ‘x-frame-options’ because of ‘frame-ancestors’ directive." warning message even when no "x-frame-options" header present</title> +</head> +<body> +<div id="cspmessage">Do not log xfo ignore warning when no xfo is set.</div> +</body> +</html> diff --git a/dom/security/test/csp/file_no_log_ignore_xfo.html^headers^ b/dom/security/test/csp/file_no_log_ignore_xfo.html^headers^ new file mode 100644 index 0000000000..1fbbf3de99 --- /dev/null +++ b/dom/security/test/csp/file_no_log_ignore_xfo.html^headers^ @@ -0,0 +1,2 @@ +Content-Security-Policy: frame-ancestors http://mochi.test:8888 +Cache-Control: no-cache diff --git a/dom/security/test/csp/file_nonce_redirector.sjs b/dom/security/test/csp/file_nonce_redirector.sjs new file mode 100644 index 0000000000..b56b9ded37 --- /dev/null +++ b/dom/security/test/csp/file_nonce_redirector.sjs @@ -0,0 +1,28 @@ +// custom *.sjs file for +// Bug 1469150:Scripts with valid nonce get blocked if URL redirects. + +const URL_PATH = "example.com/tests/dom/security/test/csp/"; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + let queryStr = request.queryString; + + if (queryStr === "redirect") { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader( + "Location", + "https://" + URL_PATH + "file_nonce_redirector.sjs?load", + false + ); + return; + } + + if (queryStr === "load") { + response.setHeader("Content-Type", "application/javascript", false); + response.write("console.log('script loaded');"); + return; + } + + // we should never get here - return something unexpected + response.write("d'oh"); +} diff --git a/dom/security/test/csp/file_nonce_redirects.html b/dom/security/test/csp/file_nonce_redirects.html new file mode 100644 index 0000000000..e291164900 --- /dev/null +++ b/dom/security/test/csp/file_nonce_redirects.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'> + <meta http-equiv="Content-Security-Policy" content="script-src 'nonce-abcd1234'"> + <title>Bug 1469150:Scripts with valid nonce get blocked if URL redirects</title> + </head> +<body> + +<script nonce='abcd1234' id='redirectScript'></script> + +<script nonce='abcd1234' type='application/javascript'> + var redirectScript = document.getElementById('redirectScript'); + redirectScript.onload = function(e) { + window.parent.postMessage({result: 'script-loaded'}, '*'); + }; + redirectScript.onerror = function(e) { + window.parent.postMessage({result: 'script-blocked'}, '*'); + } + redirectScript.src = 'file_nonce_redirector.sjs?redirect'; +</script> +</body> +</html> diff --git a/dom/security/test/csp/file_nonce_snapshot.sjs b/dom/security/test/csp/file_nonce_snapshot.sjs new file mode 100644 index 0000000000..2b114fd87e --- /dev/null +++ b/dom/security/test/csp/file_nonce_snapshot.sjs @@ -0,0 +1,54 @@ +"use strict"; + +const TEST_FRAME = `<!DOCTYPE HTML> + <html> + <body> + <script id='myScript' nonce='123456789' type='application/javascript'></script> + <script nonce='123456789'> + let myScript = document.getElementById('myScript'); + // 1) start loading the script using the nonce 123456789 + myScript.src='file_nonce_snapshot.sjs?redir-script'; + // 2) dynamically change the nonce, load should use initial nonce + myScript.setAttribute('nonce','987654321'); + </script> + </body> + </html>`; + +const SCRIPT = "window.parent.postMessage('script-loaded', '*');"; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + let queryString = request.queryString; + + if (queryString === "load-frame") { + response.setHeader( + "Content-Security-Policy", + "script-src 'nonce-123456789'", + false + ); + response.setHeader("Content-Type", "text/html", false); + response.write(TEST_FRAME); + return; + } + + if (queryString === "redir-script") { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader( + "Location", + "file_nonce_snapshot.sjs?load-script", + false + ); + return; + } + + if (queryString === "load-script") { + response.setHeader("Content-Type", "application/javascript", false); + response.write(SCRIPT); + 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_nonce_source.html b/dom/security/test/csp/file_nonce_source.html new file mode 100644 index 0000000000..01d4046c37 --- /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 allowlisted 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 allowlisted 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 allowlisted 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 0000000000..865e5fe984 --- /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 0000000000..f995688b13 --- /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_object_inherit.html b/dom/security/test/csp/file_object_inherit.html new file mode 100644 index 0000000000..76c9764162 --- /dev/null +++ b/dom/security/test/csp/file_object_inherit.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1457100: Test OBJECT inherits CSP if needed</title> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content= "img-src https://bug1457100.test.com"/> +</head> +<body> +<object id="dataObject" data="data:text/html,object<script>var foo = 0;</script>"></object> + +<script type="application/javascript"> + var dataObject = document.getElementById("dataObject"); + dataObject.onload = function () { + var contentDoc = SpecialPowers.wrap(dataObject).contentDocument; + var cspJSON = contentDoc.cspJSON; + window.parent.postMessage({cspJSON}, "*"); + } +</script> + +</body> +</html> diff --git a/dom/security/test/csp/file_parent_location_js.html b/dom/security/test/csp/file_parent_location_js.html new file mode 100644 index 0000000000..9c56f49905 --- /dev/null +++ b/dom/security/test/csp/file_parent_location_js.html @@ -0,0 +1,18 @@ +<html> +<head> + <title>Test setting parent location to javascript:</title> + <meta http-equiv="Content-Security-Policy" content="script-src 'nonce-bug1550414'"> + <script nonce="bug1550414"> + document.addEventListener("securitypolicyviolation", (e) => { + window.parent.postMessage({ + blockedURI: e.blockedURI, + violatedDirective: e.violatedDirective, + originalPolicy: e.originalPolicy, + }, '*'); + }); + </script> +</head> +<body> + <iframe src="file_iframe_parent_location_js.html"></iframe> +</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 0000000000..662fbfb8af --- /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 0000000000..09286d42e9 --- /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 0000000000..50af2b1437 --- /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 0000000000..a16cc90ec6 --- /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 0000000000..bed3a1dccf --- /dev/null +++ b/dom/security/test/csp/file_path_matching_redirect_server.sjs @@ -0,0 +1,12 @@ +// 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_pdfjs_not_subject_to_csp.html b/dom/security/test/csp/file_pdfjs_not_subject_to_csp.html new file mode 100644 index 0000000000..da5c7f0a6e --- /dev/null +++ b/dom/security/test/csp/file_pdfjs_not_subject_to_csp.html @@ -0,0 +1,21 @@ +<html> +<head> + <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'nonce-allowPDF'; base-uri 'self'"> +</head> +<body> +<iframe id="pdfFrame"></iframe> +<br/> +<button id="pdfButton">click to load pdf</button> +<script nonce="allowPDF"> + async function loadPDFIntoIframe() { + let response = await fetch("dummy.pdf"); + let blob = await response.blob(); + var blobUrl = URL.createObjectURL(blob); + var pdfFrame = document.getElementById("pdfFrame"); + pdfFrame.src = blobUrl; + } + let pdfButton = document.getElementById("pdfButton"); + pdfButton.addEventListener("click", loadPDFIntoIframe); +</script> +</body> +</html> diff --git a/dom/security/test/csp/file_ping.html b/dom/security/test/csp/file_ping.html new file mode 100644 index 0000000000..8aaf34cc3a --- /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 0000000000..2a75eef7e8 --- /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 0000000000..c4ff8ea9fd --- /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 0000000000..a5c610cd7b --- /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_punycode_host_src.js b/dom/security/test/csp/file_punycode_host_src.js new file mode 100644 index 0000000000..9728e2fecc --- /dev/null +++ b/dom/security/test/csp/file_punycode_host_src.js @@ -0,0 +1,2 @@ +const LOADED = true; +parent.postMessage({ result: "script-allowed" }, "*"); diff --git a/dom/security/test/csp/file_punycode_host_src.sjs b/dom/security/test/csp/file_punycode_host_src.sjs new file mode 100644 index 0000000000..184c6b1041 --- /dev/null +++ b/dom/security/test/csp/file_punycode_host_src.sjs @@ -0,0 +1,47 @@ +// custom *.sjs for Bug 1224225 +// Punycode in CSP host sources + +const HTML_PART1 = + "<!DOCTYPE HTML>" + + '<html><head><meta charset="utf-8">' + + "<title>Bug 1224225 - CSP source matching should work for punycoded domain names</title>" + + "</head>" + + "<body>" + + "<script id='script' src='"; + +// U+00E4 LATIN SMALL LETTER A WITH DIAERESIS, encoded as UTF-8 code units. +// response.write() writes out the provided string characters truncated to +// bytes, so "ä" literally would write a literal \xE4 byte, not the desired +// two-byte UTF-8 sequence. +const TESTCASE1 = "http://sub2.\xC3\xA4lt.example.org/"; +const TESTCASE2 = "http://sub2.xn--lt-uia.example.org/"; + +const HTML_PART2 = + "tests/dom/security/test/csp/file_punycode_host_src.js'></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); + + Components.utils.importGlobalProperties(["URLSearchParams"]); + const query = new URLSearchParams(request.queryString); + + if (query.get("csp")) { + response.setHeader("Content-Security-Policy", query.get("csp"), false); + } + if (query.get("action") == "script-unicode-csp-punycode") { + response.write(HTML_PART1 + TESTCASE1 + HTML_PART2); + return; + } + if (query.get("action") == "script-punycode-csp-punycode") { + response.write(HTML_PART1 + TESTCASE2 + HTML_PART2); + 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_redirect_content.sjs b/dom/security/test/csp/file_redirect_content.sjs new file mode 100644 index 0000000000..f1eab21b08 --- /dev/null +++ b/dom/security/test/csp/file_redirect_content.sjs @@ -0,0 +1,41 @@ +// 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 0000000000..9cc7e65486 --- /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 0000000000..37c721be24 --- /dev/null +++ b/dom/security/test/csp/file_redirect_worker.sjs @@ -0,0 +1,35 @@ +// 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 0000000000..d05af88fe8 --- /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 0000000000..c7a8b72520 --- /dev/null +++ b/dom/security/test/csp/file_redirects_page.sjs @@ -0,0 +1,141 @@ +// 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 0000000000..f1bb86c2cd --- /dev/null +++ b/dom/security/test/csp/file_redirects_resource.sjs @@ -0,0 +1,172 @@ +// 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_report.html b/dom/security/test/csp/file_report.html new file mode 100644 index 0000000000..fb18af8057 --- /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 0000000000..32850495df --- /dev/null +++ b/dom/security/test/csp/file_report_chromescript.js @@ -0,0 +1,64 @@ +/* eslint-env mozilla/chrome-script */ + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +Cu.importGlobalProperties(["TextDecoder"]); + +const reportURI = "http://mochi.test:8888/foo.sjs"; + +var openingObserver = { + observe(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); + + let bytes = NetUtil.readInputStream(binstream); + + // 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); + + let textDecoder = new TextDecoder(); + reportText = textDecoder.decode(bytes); + } + + message.report = reportText; + } catch (e) { + message.error = e.toString(); + } + + sendAsyncMessage("opening-request-completed", message); + } + }, +}; + +Services.obs.addObserver(openingObserver, "http-on-opening-request"); +addMessageListener("finish", function() { + Services.obs.removeObserver(openingObserver, "http-on-opening-request"); +}); diff --git a/dom/security/test/csp/file_report_font_cache-1.html b/dom/security/test/csp/file_report_font_cache-1.html new file mode 100644 index 0000000000..59b4908f83 --- /dev/null +++ b/dom/security/test/csp/file_report_font_cache-1.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<style> +@font-face { + font-family: "CSP Report Test Font 1"; + src: url(Ahem.ttf?report_font_cache-1); +} +@font-face { + font-family: "CSP Report Test Font 2"; + src: url(Ahem.ttf?report_font_cache-2); +} +@font-face { + font-family: "CSP Report Test Font 3"; + src: url(Ahem.ttf?report_font_cache-3); +} +.x { font: 24px "CSP Report Test Font 1"; } +.y { font: 24px "CSP Report Test Font 2"; } +.z { font: 24px "CSP Report Test Font 3"; } +</style> +<p class=x>A</p> +<p class=y>A</p> +<p class=z>A</p> +<script> +// Wait until the fonts would have been added to the user font cache. +document.body.offsetWidth; +document.fonts.ready.then(() => window.parent.postMessage("first-doc-ready", "*")); +</script> diff --git a/dom/security/test/csp/file_report_font_cache-2.html b/dom/security/test/csp/file_report_font_cache-2.html new file mode 100644 index 0000000000..cea9cea663 --- /dev/null +++ b/dom/security/test/csp/file_report_font_cache-2.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<style> +@font-face { + font-family: "CSP Report Test Font 1"; + src: url(Ahem.ttf?report_font_cache-1); +} +@font-face { + font-family: "CSP Report Test Font 3"; + src: url(Ahem.ttf?report_font_cache-3); +} +p { margin-right: 1ex; } /* cause cached CSP check to happen OMT (due to + font metrics lookup) */ +.x { font: 24px "CSP Report Test Font 1"; } +.y { font: 24px "CSP Report Test Font 3"; } +</style> +<p class="x">A</p> +<script> +// First flush should dispatch the "Test Font 1" report that is stored +// in the user font cache. +document.body.offsetWidth; + +// Second flush should dispatch "Test Font 3" report. +document.querySelector("p").className = "y"; +document.body.offsetWidth; +</script> diff --git a/dom/security/test/csp/file_report_font_cache-2.html^headers^ b/dom/security/test/csp/file_report_font_cache-2.html^headers^ new file mode 100644 index 0000000000..493f850baa --- /dev/null +++ b/dom/security/test/csp/file_report_font_cache-2.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: font-src 'none'; report-uri http://mochi.test:8888/foo.sjs 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 0000000000..b578b77b33 --- /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 0000000000..77a36faea1 --- /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 0000000000..c540935487 --- /dev/null +++ b/dom/security/test/csp/file_report_for_import_server.sjs @@ -0,0 +1,50 @@ +// 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 0000000000..e69de29bb2 --- /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 0000000000..3f2fdfe9e6 --- /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_ro_ignore_xfo.html b/dom/security/test/csp/file_ro_ignore_xfo.html new file mode 100644 index 0000000000..85e7f0092c --- /dev/null +++ b/dom/security/test/csp/file_ro_ignore_xfo.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1024557: Ignore x-frame-options if CSP with frame-ancestors exists</title> +</head> +<body> +<div id="cspmessage">Ignoring XFO because of CSP_RO</div> +</body> +</html>
\ No newline at end of file diff --git a/dom/security/test/csp/file_ro_ignore_xfo.html^headers^ b/dom/security/test/csp/file_ro_ignore_xfo.html^headers^ new file mode 100644 index 0000000000..ab8366f061 --- /dev/null +++ b/dom/security/test/csp/file_ro_ignore_xfo.html^headers^ @@ -0,0 +1,3 @@ +Content-Security-Policy-Report-Only: frame-ancestors http://mochi.test:8888 +X-Frame-Options: deny +Cache-Control: no-cache 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 0000000000..ce1e80c865 --- /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 0000000000..f934497eee --- /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 0000000000..087b5651a9 --- /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}, "*"); + } + + 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 0000000000..79631bd394 --- /dev/null +++ b/dom/security/test/csp/file_sandbox_12.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.postMessage({ok: result, 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 0000000000..96286db8d5 --- /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}, "*"); + } + + 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 0000000000..b37aa1bcef --- /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 0000000000..ba808e47d5 --- /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 0000000000..b2d4ed0940 --- /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 0000000000..c08849b689 --- /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}, "*"); + } + + 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 0000000000..44705f4d2b --- /dev/null +++ b/dom/security/test/csp/file_sandbox_6.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.postMessage({ok: result, 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 0000000000..3b249d4101 --- /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 0000000000..4f9cd89161 --- /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 0000000000..29ffc191cd --- /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 0000000000..faab9f0fc6 --- /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 0000000000..4705ce9ded --- /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 0000000000..7f43927ddf --- /dev/null +++ b/dom/security/test/csp/file_sandbox_fail.js @@ -0,0 +1,7 @@ +function ok(result, desc) { + window.parent.postMessage({ ok: result, 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 0000000000..c06341a082 --- /dev/null +++ b/dom/security/test/csp/file_sandbox_pass.js @@ -0,0 +1,7 @@ +function ok(result, desc) { + window.parent.postMessage({ ok: result, 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 0000000000..09286d42e9 --- /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 0000000000..c32f773484 --- /dev/null +++ b/dom/security/test/csp/file_scheme_relative_sources.sjs @@ -0,0 +1,45 @@ +/** + * Custom *.sjs specifically for the needs of + * Bug 921493 - CSP: test allowlisting 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_script_template.html b/dom/security/test/csp/file_script_template.html new file mode 100644 index 0000000000..3819592912 --- /dev/null +++ b/dom/security/test/csp/file_script_template.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html> +<head> +<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline'"> +<template id="a"> + <script src="file_script_template.js"></script> +</template> +</head> +<body> +<script> + var temp = document.getElementsByTagName("template")[0]; + var clon = temp.content.cloneNode(true); + document.body.appendChild(clon); +</script> +</body> +</html> diff --git a/dom/security/test/csp/file_script_template.js b/dom/security/test/csp/file_script_template.js new file mode 100644 index 0000000000..d75869f763 --- /dev/null +++ b/dom/security/test/csp/file_script_template.js @@ -0,0 +1 @@ +// dummy *.js file 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 0000000000..16196bb19f --- /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 0000000000..26af7ed9b5 --- /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 0000000000..13202c65ff --- /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 0000000000..b819946983 --- /dev/null +++ b/dom/security/test/csp/file_service_worker.html @@ -0,0 +1,21 @@ +<!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}, "*"); + } + + const promises = [ + navigator.serviceWorker.ready, + navigator.serviceWorker.register("file_service_worker.js", { scope: "." }) + ]; + + Promise.race(promises).then(finish.bind(null, 'allowed'), + 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 0000000000..1bf583f4cc --- /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_spawn_service_worker.js b/dom/security/test/csp/file_spawn_service_worker.js new file mode 100644 index 0000000000..b262fa10a3 --- /dev/null +++ b/dom/security/test/csp/file_spawn_service_worker.js @@ -0,0 +1 @@ +// dummy file diff --git a/dom/security/test/csp/file_spawn_shared_worker.js b/dom/security/test/csp/file_spawn_shared_worker.js new file mode 100644 index 0000000000..56b33f4aef --- /dev/null +++ b/dom/security/test/csp/file_spawn_shared_worker.js @@ -0,0 +1,7 @@ +onconnect = function(e) { + var port = e.ports[0]; + port.addEventListener("message", function(e) { + port.postMessage("shared worker is executing"); + }); + port.start(); +}; diff --git a/dom/security/test/csp/file_spawn_worker.js b/dom/security/test/csp/file_spawn_worker.js new file mode 100644 index 0000000000..acde7408c1 --- /dev/null +++ b/dom/security/test/csp/file_spawn_worker.js @@ -0,0 +1 @@ +postMessage("worker is executing"); 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 0000000000..09286d42e9 --- /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 0000000000..0ea79e2a96 --- /dev/null +++ b/dom/security/test/csp/file_strict_dynamic_default_src.html @@ -0,0 +1,20 @@ +<!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" data-result="blocked"> +<script> +let img = document.getElementById("testimage"); +img.addEventListener("load", function() { + img.dataset.result = "allowed"; +}, { once: true }); +</script> + +</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 0000000000..09286d42e9 --- /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 0000000000..bd53b0adb2 --- /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 0000000000..c51fefd72e --- /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 0000000000..10a0f32e4b --- /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 0000000000..2a3a7d4998 --- /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 0000000000..9938ef2dcd --- /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 0000000000..0889583821 --- /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_marquee.html b/dom/security/test/csp/file_strict_dynamic_script_events_marquee.html new file mode 100644 index 0000000000..701ef32269 --- /dev/null +++ b/dom/security/test/csp/file_strict_dynamic_script_events_marquee.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 0000000000..94b6aefb19 --- /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 0000000000..d17a58f279 --- /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 0000000000..f0b26da915 --- /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 0000000000..3ba970ce84 --- /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 0000000000..233b359310 --- /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_svg_inline_style_base.html b/dom/security/test/csp/file_svg_inline_style_base.html new file mode 100644 index 0000000000..e6fe3699d4 --- /dev/null +++ b/dom/security/test/csp/file_svg_inline_style_base.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + <img src="file_svg_inline_style_server.sjs?svg_inline_style"> +</body> +</html> diff --git a/dom/security/test/csp/file_svg_inline_style_csp.html b/dom/security/test/csp/file_svg_inline_style_csp.html new file mode 100644 index 0000000000..c37b900863 --- /dev/null +++ b/dom/security/test/csp/file_svg_inline_style_csp.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content="default-src 'self'"> +</head> +<body> + <img src="file_svg_inline_style_server.sjs?svg_inline_style_csp"> +</body> +</html> diff --git a/dom/security/test/csp/file_svg_inline_style_server.sjs b/dom/security/test/csp/file_svg_inline_style_server.sjs new file mode 100644 index 0000000000..5acdbbd155 --- /dev/null +++ b/dom/security/test/csp/file_svg_inline_style_server.sjs @@ -0,0 +1,43 @@ +"use strict"; + +const SVG_IMG = `<svg width="200" height="200" viewBox="0 0 150 150" xmlns="http://www.w3.org/2000/svg"> + <style> + circle { + fill: orange; + stroke: black; + stroke-width: 10px; + } + </style> + <circle cx="50" cy="50" r="40" /> + </svg>`; + +const SVG_IMG_NO_INLINE_STYLE = `<svg width="200" height="200" viewBox="0 0 150 150" xmlns="http://www.w3.org/2000/svg"> + <circle cx="50" cy="50" r="40" /> + </svg>`; + +function handleRequest(request, response) { + const query = request.queryString; + + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "image/svg+xml", false); + + if (query === "svg_inline_style_csp") { + response.setHeader("Content-Security-Policy", "default-src 'none'", false); + response.write(SVG_IMG); + return; + } + + if (query === "svg_inline_style") { + response.write(SVG_IMG); + return; + } + + if (query === "svg_no_inline_style") { + response.write(SVG_IMG_NO_INLINE_STYLE); + 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_test_browser_bookmarklets.html b/dom/security/test/csp/file_test_browser_bookmarklets.html new file mode 100644 index 0000000000..cb12e4efd0 --- /dev/null +++ b/dom/security/test/csp/file_test_browser_bookmarklets.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta http-equiv="X-UA-Compatible" content="ie=edge"> + <title>Document</title> +</head> +<body> + <h1>Test-Document</h1> +</body> +</html>
\ No newline at end of file diff --git a/dom/security/test/csp/file_test_browser_bookmarklets.html^headers^ b/dom/security/test/csp/file_test_browser_bookmarklets.html^headers^ new file mode 100644 index 0000000000..e138f234fb --- /dev/null +++ b/dom/security/test/csp/file_test_browser_bookmarklets.html^headers^ @@ -0,0 +1,2 @@ +Content-Security-Policy: script-src 'none' +Cache-Control: no-cache diff --git a/dom/security/test/csp/file_testserver.sjs b/dom/security/test/csp/file_testserver.sjs new file mode 100644 index 0000000000..0363fc2c7a --- /dev/null +++ b/dom/security/test/csp/file_testserver.sjs @@ -0,0 +1,67 @@ +// SJS file for CSP mochitests +"use strict"; +const { NetUtil } = ChromeUtils.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.nsIFile); + + 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 + let type = "text/html"; + if (query.has("type")) { + type = query.get("type"); + } + + response.setHeader("Content-Type", type, false); + if (query.has("file")) { + response.write(loadHTMLFromFile(query.get("file"))); + } +} diff --git a/dom/security/test/csp/file_uir_top_nav.html b/dom/security/test/csp/file_uir_top_nav.html new file mode 100644 index 0000000000..28263e9db7 --- /dev/null +++ b/dom/security/test/csp/file_uir_top_nav.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> +</head> +<body> +<script class="testbody" type="text/javascript"> + +// 1) same origin navigation +window.open("http://example.com/tests/dom/security/test/csp/file_uir_top_nav_dummy.html"); + +// 2) same origin navigation +window.open("http://test1.example.com/tests/dom/security/test/csp/file_uir_top_nav_dummy.html"); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/file_uir_top_nav_dummy.html b/dom/security/test/csp/file_uir_top_nav_dummy.html new file mode 100644 index 0000000000..65762f1c71 --- /dev/null +++ b/dom/security/test/csp/file_uir_top_nav_dummy.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<body> +just a dummy page to check uir applies to top level navigations +<script class="testbody" type="text/javascript"> +window.onload = function() { + window.opener.parent.postMessage({result: window.location.href}, "*"); + window.close(); +} +</script> +</body> +</html> 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 0000000000..9c4ba597f3 --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure.html @@ -0,0 +1,90 @@ +<!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"> + // WebSocket tests are not supported on Android yet. Bug 1566168 + const { AppConstants } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + if (AppConstants.platform !== "android") { + 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.close(); + }; + mySocket.onerror = function(e) { + // debug information for Bug 1316305 + dump(" xxx mySocket.onerror: (mySocket): " + mySocket + "\n"); + dump(" xxx mySocket.onerror: (mySocket.url): " + mySocket.url + "\n"); + dump(" xxx mySocket.onerror: (e): " + e + "\n"); + dump(" xxx mySocket.onerror: (e.message): " + e.message + "\n"); + 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 0000000000..e675c62e9f --- /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 0000000000..83957560c3 --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_cors_server.sjs @@ -0,0 +1,61 @@ +// 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 0000000000..a7fb0a2176 --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_docwrite_iframe.sjs @@ -0,0 +1,55 @@ +// 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_loopback.html b/dom/security/test/csp/file_upgrade_insecure_loopback.html new file mode 100644 index 0000000000..b824604b6e --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_loopback.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1447784 - Implement CSP upgrade-insecure-requests directive</title> +</head> +<body> + +<script type="text/javascript"> + // === TEST 1 + var url1 = "http://127.0.0.1:8888/tests/dom/security/test/csp/file_upgrade_insecure_loopback_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(); + +</script> + +</body> +</html> diff --git a/dom/security/test/csp/file_upgrade_insecure_loopback_form.html b/dom/security/test/csp/file_upgrade_insecure_loopback_form.html new file mode 100644 index 0000000000..ed6b3b8542 --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_loopback_form.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1661423 - don't apply upgrade-insecure-requests on form submissions to localhost</title> +</head> +<body> + +<form id="form" action="http://127.0.0.1/bug-1661423-dont-upgrade-localhost"> + <input type="submit"> +</form>> +<script type="text/javascript"> + + form.submit(); +</script> +</body> +</html> diff --git a/dom/security/test/csp/file_upgrade_insecure_loopback_server.sjs b/dom/security/test/csp/file_upgrade_insecure_loopback_server.sjs new file mode 100644 index 0000000000..ff7931a1d4 --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_loopback_server.sjs @@ -0,0 +1,22 @@ +// Custom *.sjs file specifically for the needs of Bug: +// Bug 1447784 - Implement CSP upgrade-insecure-requests directive + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Headers", "content-type", false); + response.setHeader("Access-Control-Allow-Methods", "GET", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + + // 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; + } else { + response.write("request-is-https"); + } + + // 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_meta.html b/dom/security/test/csp/file_upgrade_insecure_meta.html new file mode 100644 index 0000000000..a84a8c254d --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_meta.html @@ -0,0 +1,86 @@ +<!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"> + // WebSocket tests are not supported on Android Yet. Bug 1566168. + const { AppConstants } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + if (AppConstants.platform !== "android") { + 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.close(); + }; + 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_navigation.sjs b/dom/security/test/csp/file_upgrade_insecure_navigation.sjs new file mode 100644 index 0000000000..51afa39bf7 --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_navigation.sjs @@ -0,0 +1,79 @@ +// Custom *.sjs file specifically for the needs of +// https://bugzilla.mozilla.org/show_bug.cgi?id=1271173 + +"use strict"; +Components.utils.importGlobalProperties(["URLSearchParams"]); + +const TEST_NAVIGATIONAL_UPGRADE = ` + <!DOCTYPE html> + <html> + <head><meta charset="utf-8"></head> + <body> + <a href="http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation.sjs?action=framenav" id="testlink">clickme</a> + <script type="text/javascript"> + // before navigating the current frame we open the window and check that uir applies + var myWin = window.open("http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation.sjs?action=docnav"); + + window.addEventListener("message", receiveMessage, false); + function receiveMessage(event) { + myWin.close(); + var link = document.getElementById('testlink'); + link.click(); + } + </script> + </body> + </html>`; + +const FRAME_NAV = ` + <!DOCTYPE html> + <html> + <head><meta charset="utf-8"></head> + <body> + <script type="text/javascript"> + parent.postMessage({result: document.documentURI}, "*"); + </script> + </body> + </html>`; + +const DOC_NAV = ` + <!DOCTYPE html> + <html> + <head><meta charset="utf-8"></head> + <body> + <script type="text/javascript"> + // call back to the main testpage signaling whether the upgraded succeeded + window.opener.parent.postMessage({result: document.documentURI}, "*"); + // let the opener (iframe) now that we can now close the window and move on with the test. + window.opener.postMessage({result: "readyToMoveOn"}, "*"); + </script> + </body> + </html>`; + +function handleRequest(request, response) { + const query = new URLSearchParams(request.queryString); + + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + if (query.get("csp")) { + response.setHeader("Content-Security-Policy", query.get("csp"), false); + } + + if (query.get("action") === "perform_navigation") { + response.write(TEST_NAVIGATIONAL_UPGRADE); + return; + } + + if (query.get("action") === "framenav") { + response.write(FRAME_NAV); + return; + } + + if (query.get("action") === "docnav") { + response.write(DOC_NAV); + 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_navigation_redirect.sjs b/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs new file mode 100644 index 0000000000..3f7f8158e0 --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs @@ -0,0 +1,50 @@ +"use strict"; + +const FINAL_DOCUMENT = ` + <html> + <body> + final document + <script> + window.onload = function() { + let docURI = document.documentURI; + window.opener.parent.postMessage({docURI}, "*"); + window.close(); + } + </script> + </body> + </html>`; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + const query = request.queryString; + + if (query === "same_origin_redirect") { + let newLocation = + "http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs?finaldoc_same_origin_redirect"; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", newLocation, false); + return; + } + + if (query === "cross_origin_redirect") { + let newLocation = + "http://test1.example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs?finaldoc_cross_origin_redirect"; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", newLocation, false); + return; + } + + if ( + query === "finaldoc_same_origin_redirect" || + query === "finaldoc_cross_origin_redirect" + ) { + response.write(FINAL_DOCUMENT); + 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_navigation_redirect_cross_origin.html b/dom/security/test/csp/file_upgrade_insecure_navigation_redirect_cross_origin.html new file mode 100644 index 0000000000..dff2c9faf3 --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_navigation_redirect_cross_origin.html @@ -0,0 +1,10 @@ +<html> +<head> + <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" /> +</head> +<body> +<script> + window.open("http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs?cross_origin_redirect"); +</script> +</body> +</html> diff --git a/dom/security/test/csp/file_upgrade_insecure_navigation_redirect_same_origin.html b/dom/security/test/csp/file_upgrade_insecure_navigation_redirect_same_origin.html new file mode 100644 index 0000000000..811850e08c --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_navigation_redirect_same_origin.html @@ -0,0 +1,10 @@ +<html> +<head> + <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" /> +</head> +<body> +<script> + window.open("http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs?same_origin_redirect"); +</script> +</body> +</html> 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 0000000000..c78e9a784d --- /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 0000000000..6cc2086fe3 --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_reporting_server.sjs @@ -0,0 +1,87 @@ +// Custom *.sjs specifically for the needs of Bug +// Bug 1139297 - Implement CSP upgrade-insecure-requests directive + +const { NetUtil } = ChromeUtils.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.nsIFile); + 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 0000000000..d884a9000d --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_server.sjs @@ -0,0 +1,112 @@ +// 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 0000000000..b7159c742b --- /dev/null +++ b/dom/security/test/csp/file_upgrade_insecure_wsh.py @@ -0,0 +1,6 @@ +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 0000000000..0f6a67460e --- /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 0000000000..917ab73ef2 --- /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 0000000000..e0e00c4be0 --- /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 0000000000..b0ff9ef853 --- /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 0000000000..917ab73ef2 --- /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 0000000000..55f17c0f92 --- /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 0000000000..7ecf8eec43 --- /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/file_websocket_csp_upgrade.html b/dom/security/test/csp/file_websocket_csp_upgrade.html new file mode 100644 index 0000000000..9302a6e637 --- /dev/null +++ b/dom/security/test/csp/file_websocket_csp_upgrade.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1729897: Allow unsecure websocket from localhost page with CSP: upgrade-insecure </title>
+ <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
+</head>
+<body>
+ <script type="application/javascript">
+ /* load socket using ws */
+ var wsSocket = new WebSocket("ws://localhost/tests/dom/security/test/csp/file_websocket_self");
+ wsSocket.onopen = function(e) {
+ window.parent.postMessage({result: "self-ws-loaded", url: wsSocket.url}, "*");
+ };
+ wsSocket.onerror = function(e) {
+ window.parent.postMessage({result: "self-ws-blocked"}, "*");
+ };
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_websocket_explicit.html b/dom/security/test/csp/file_websocket_explicit.html new file mode 100644 index 0000000000..51462ab741 --- /dev/null +++ b/dom/security/test/csp/file_websocket_explicit.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1345615: Allow websocket schemes when using 'self' in CSP</title>
+ <meta http-equiv="Content-Security-Policy" content="connect-src ws:">
+</head>
+<body>
+ <script type="application/javascript">
+ /* load socket using ws */
+ var wsSocket = new WebSocket("ws://example.com/tests/dom/security/test/csp/file_websocket_self");
+ wsSocket.onopen = function(e) {
+ window.parent.postMessage({result: "explicit-ws-loaded"}, "*");
+ wsSocket.close();
+ };
+ wsSocket.onerror = function(e) {
+ window.parent.postMessage({result: "explicit-ws-blocked"}, "*");
+ };
+
+ /* load socket using wss */
+ var wssSocket = new WebSocket("wss://example.com/tests/dom/security/test/csp/file_websocket_self");
+ wssSocket.onopen = function(e) {
+ window.parent.postMessage({result: "explicit-wss-loaded"}, "*");
+ wssSocket.close();
+ };
+ wssSocket.onerror = function(e) {
+ window.parent.postMessage({result: "explicit-wss-blocked"}, "*");
+ };
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_websocket_self.html b/dom/security/test/csp/file_websocket_self.html new file mode 100644 index 0000000000..3ff5f05580 --- /dev/null +++ b/dom/security/test/csp/file_websocket_self.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1345615: Allow websocket schemes when using 'self' in CSP</title>
+ <meta http-equiv="Content-Security-Policy" content="connect-src 'self'">
+</head>
+<body>
+ <script type="application/javascript">
+ /* load socket using ws */
+ var wsSocket = new WebSocket("ws://example.com/tests/dom/security/test/csp/file_websocket_self");
+ wsSocket.onopen = function(e) {
+ window.parent.postMessage({result: "self-ws-loaded"}, "*");
+ wsSocket.close();
+ };
+ wsSocket.onerror = function(e) {
+ window.parent.postMessage({result: "self-ws-blocked"}, "*");
+ };
+
+ /* load socket using wss */
+ var wssSocket = new WebSocket("wss://example.com/tests/dom/security/test/csp/file_websocket_self");
+ wssSocket.onopen = function(e) {
+ window.parent.postMessage({result: "self-wss-loaded"}, "*");
+ wssSocket.close();
+ };
+ wssSocket.onerror = function(e) {
+ window.parent.postMessage({result: "self-wss-blocked"}, "*");
+ };
+ </script>
+</body>
+</html>
diff --git a/dom/security/test/csp/file_websocket_self_wsh.py b/dom/security/test/csp/file_websocket_self_wsh.py new file mode 100644 index 0000000000..eb45e224f3 --- /dev/null +++ b/dom/security/test/csp/file_websocket_self_wsh.py @@ -0,0 +1,6 @@ +def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ pass
diff --git a/dom/security/test/csp/file_win_open_blocked.html b/dom/security/test/csp/file_win_open_blocked.html new file mode 100644 index 0000000000..2d0828a872 --- /dev/null +++ b/dom/security/test/csp/file_win_open_blocked.html @@ -0,0 +1,3 @@ +<script> + window.opener.postMessage('window-opened', '*'); +</script> diff --git a/dom/security/test/csp/file_windowwatcher_frameA.html b/dom/security/test/csp/file_windowwatcher_frameA.html new file mode 100644 index 0000000000..9e544142ce --- /dev/null +++ b/dom/security/test/csp/file_windowwatcher_frameA.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<body> +frame A<br/> +<iframe name='frameB' src="http://example.com/tests/dom/security/test/csp/file_windowwatcher_subframeB.html"></iframe> +<iframe name='frameC' src="http://example.com/tests/dom/security/test/csp/file_windowwatcher_subframeC.html"></iframe> +<iframe name='frameD' src="http://example.com/tests/dom/security/test/csp/file_windowwatcher_subframeD.html"></iframe> + +<script class="testbody" type="text/javascript"> + +window.onload = function() { + frameB.openWin(); +} + +</script> +</body> +</html> diff --git a/dom/security/test/csp/file_windowwatcher_subframeB.html b/dom/security/test/csp/file_windowwatcher_subframeB.html new file mode 100644 index 0000000000..e7ef422313 --- /dev/null +++ b/dom/security/test/csp/file_windowwatcher_subframeB.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<body> +subFrame B + +<script> +function openWin() { + parent.frameC.open.call(parent.frameD, "http://example.com/tests/dom/security/test/csp/file_windowwatcher_win_open.html"); +} +</script> +</body> +</html> diff --git a/dom/security/test/csp/file_windowwatcher_subframeC.html b/dom/security/test/csp/file_windowwatcher_subframeC.html new file mode 100644 index 0000000000..b97c40432e --- /dev/null +++ b/dom/security/test/csp/file_windowwatcher_subframeC.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> +</head> +<body> +subFrame C +</body> +</html> diff --git a/dom/security/test/csp/file_windowwatcher_subframeD.html b/dom/security/test/csp/file_windowwatcher_subframeD.html new file mode 100644 index 0000000000..2f778ea4cd --- /dev/null +++ b/dom/security/test/csp/file_windowwatcher_subframeD.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<html> +<body> +subFrame D +</body> +</html> diff --git a/dom/security/test/csp/file_windowwatcher_win_open.html b/dom/security/test/csp/file_windowwatcher_win_open.html new file mode 100644 index 0000000000..0237e49377 --- /dev/null +++ b/dom/security/test/csp/file_windowwatcher_win_open.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<body> +Opened Window<br/> + +<script> + +window.onload = function() { + window.opener.parent.parent.postMessage({result: window.location.href}, "*"); + window.close(); +} + +</script> +</body> +</html> diff --git a/dom/security/test/csp/file_worker_src.js b/dom/security/test/csp/file_worker_src.js new file mode 100644 index 0000000000..cf45ba4ec3 --- /dev/null +++ b/dom/security/test/csp/file_worker_src.js @@ -0,0 +1,73 @@ +var mySharedWorker = new SharedWorker("file_spawn_shared_worker.js"); +mySharedWorker.port.onmessage = function(ev) { + parent.postMessage( + { + result: "shared-worker-allowed", + href: document.location.href, + }, + "*" + ); + mySharedWorker.port.close(); +}; +mySharedWorker.onerror = function(evt) { + evt.preventDefault(); + parent.postMessage( + { + result: "shared-worker-blocked", + href: document.location.href, + }, + "*" + ); + mySharedWorker.port.close(); +}; +mySharedWorker.port.start(); +mySharedWorker.port.postMessage("foo"); + +// -------------------------------------------- + +let myWorker = new Worker("file_spawn_worker.js"); +myWorker.onmessage = function(event) { + parent.postMessage( + { + result: "worker-allowed", + href: document.location.href, + }, + "*" + ); +}; +myWorker.onerror = function(event) { + parent.postMessage( + { + result: "worker-blocked", + href: document.location.href, + }, + "*" + ); +}; + +// -------------------------------------------- + +navigator.serviceWorker + .register("file_spawn_service_worker.js") + .then(function(reg) { + // registration worked + reg.unregister().then(function() { + parent.postMessage( + { + result: "service-worker-allowed", + href: document.location.href, + }, + "*" + ); + }); + }) + .catch(function(error) { + // registration failed + parent.postMessage( + { + result: "service-worker-blocked", + href: document.location.href, + }, + "*" + ); + }); diff --git a/dom/security/test/csp/file_worker_src_child_governs.html b/dom/security/test/csp/file_worker_src_child_governs.html new file mode 100644 index 0000000000..ca8a683aac --- /dev/null +++ b/dom/security/test/csp/file_worker_src_child_governs.html @@ -0,0 +1,9 @@ +<html> +<head> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content="child-src https://example.com; script-src 'nonce-foo'">"; +</head> +<body> +<script type="text/javascript" src="file_worker_src.js" nonce="foo"></script> +</body> +</html> diff --git a/dom/security/test/csp/file_worker_src_script_governs.html b/dom/security/test/csp/file_worker_src_script_governs.html new file mode 100644 index 0000000000..0385fee57c --- /dev/null +++ b/dom/security/test/csp/file_worker_src_script_governs.html @@ -0,0 +1,9 @@ +<html> +<head> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content="script-src 'nonce-foo' https://example.com">"; +</head> +<body> +<script type="text/javascript" src="file_worker_src.js" nonce="foo"></script> +</body> +</html> diff --git a/dom/security/test/csp/file_worker_src_worker_governs.html b/dom/security/test/csp/file_worker_src_worker_governs.html new file mode 100644 index 0000000000..93c8f61225 --- /dev/null +++ b/dom/security/test/csp/file_worker_src_worker_governs.html @@ -0,0 +1,9 @@ +<html> +<head> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" content="worker-src https://example.com; child-src 'none'; script-src 'nonce-foo'">"; +</head> +<body> +<script type="text/javascript" src="file_worker_src.js" nonce="foo"></script> +</body> +</html> diff --git a/dom/security/test/csp/file_xslt_inherits_csp.xml b/dom/security/test/csp/file_xslt_inherits_csp.xml new file mode 100644 index 0000000000..a6d99c3081 --- /dev/null +++ b/dom/security/test/csp/file_xslt_inherits_csp.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet type="text/xsl" href="file_xslt_inherits_csp.xsl"?> + +<root> + <t>This is some Title</t> +</root> diff --git a/dom/security/test/csp/file_xslt_inherits_csp.xml^headers^ b/dom/security/test/csp/file_xslt_inherits_csp.xml^headers^ new file mode 100644 index 0000000000..635af0a4d9 --- /dev/null +++ b/dom/security/test/csp/file_xslt_inherits_csp.xml^headers^ @@ -0,0 +1,2 @@ +Content-Security-Policy: script-src 'self' +Cache-Control: no-cache diff --git a/dom/security/test/csp/file_xslt_inherits_csp.xsl b/dom/security/test/csp/file_xslt_inherits_csp.xsl new file mode 100644 index 0000000000..82a4b0ad97 --- /dev/null +++ b/dom/security/test/csp/file_xslt_inherits_csp.xsl @@ -0,0 +1,26 @@ +<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.1"> + <xsl:output method="html"/> + <xsl:variable name="title" select="/root/t"/> + <xsl:template match="/"> + <html> + <head> + <title> + <xsl:value-of select="$title"/> + </title> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> + </head> + <body> + <p> + Below is some inline JavaScript generating some red text. + </p> + + <p id="bug"/> + <script> + document.body.append("JS DID EXCECUTE"); + </script> + + <a onClick='document.body.append("JS DID EXCECUTE");' href="#">link with lineOnClick</a> + </body> + </html> + </xsl:template> +</xsl:stylesheet> diff --git a/dom/security/test/csp/main_csp_worker.html b/dom/security/test/csp/main_csp_worker.html new file mode 100644 index 0000000000..8957e3fd25 --- /dev/null +++ b/dom/security/test/csp/main_csp_worker.html @@ -0,0 +1,439 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Bug 1475849: Test CSP worker inheritance</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="worker_helper.js"></script> + + </head> + <body> + <script type="application/javascript"> + const SJS = "worker.sjs"; + const SAME_BASE = "http://mochi.test:8888/tests/dom/security/test/csp/file_CSP.sjs"; + const CROSS_BASE = "http://example.com/tests/dom/security/test/csp/file_CSP.sjs"; + + SimpleTest.waitForExplicitFinish(); + /* test data format : + { + id: test id, short description of test, + base: URL of the request in worker, + action: type of request in worker (fetch, xhr, importscript) + type: how do we create the worker, from URL or Blob, + csp: csp of worker, + child: how do we create the child worker, from URL or Blob, + childCsp: csp of child worker + expectedBlock: result when CSP policy, true or false + } + */ + + // Document's CSP is defined in main_csp_worker.html^headers^ + // Content-Security-Policy: default-src 'self' blob: 'unsafe-inline' + var tests = [ + // create new Worker(url), worker's csp should be deliveried from header. + // csp should be: default-src 'self' blob: ; connect-src CROSS_BASE + { + id: "worker_url_fetch_same_bad", + base: SAME_BASE, + action: "fetch", + type: "url", + csp: "default-src 'self' blob: ; connect-src http://example.com", + expectBlocked: true + }, + { + id: "worker_url_importScripts_same_good", + base: SAME_BASE, + action: "importScripts", + type: "url", + csp: "default-src 'self' blob: ; connect-src http://example.com", + expectBlocked: false + }, + { + id: "worker_url_xhr_same_bad", + base: SAME_BASE, + action: "xhr", + type: "url", + csp: "default-src 'self' blob: ; connect-src http://example.com", + expectBlocked: true + }, + { + id: "worker_url_fetch_cross_good", + base: CROSS_BASE, + action: "fetch", + type: "url", + csp: "default-src 'self' blob: ; connect-src http://example.com", + expectBlocked: false + }, + { + id: "worker_url_importScripts_cross_bad", + base: CROSS_BASE, + action: "importScripts", + type: "url", + csp: "default-src 'self' blob: ; connect-src http://example.com", + expectBlocked: true + }, + { + id: "worker_url_xhr_cross_good", + base: CROSS_BASE, + action: "xhr", + type: "url", + csp: "default-src 'self' blob: ; connect-src http://example.com", + expectBlocked: false + }, + + // create new Worker(blob:), worker's csp should be inherited from + // document. + // csp should be : default-src 'self' blob: 'unsafe-inline' + { + id: "worker_blob_fetch_same_good", + base: SAME_BASE, + action: "fetch", + type: "blob", + csp: "default-src 'self' blob: ; connect-src http://example.com", + expectBlocked: false + }, + { + id: "worker_blob_xhr_same_good", + base: SAME_BASE, + action: "xhr", + type: "blob", + csp: "default-src 'self' blob: ; connect-src http://example.com", + expectBlocked: false + }, + { + id: "worker_blob_importScripts_same_good", + base: SAME_BASE, + action: "importScripts", + type: "blob", + csp: "default-src 'self' blob: ; connect-src http://example.com", + expectBlocked: false + }, + { + id: "worker_blob_fetch_cross_bad", + base: CROSS_BASE, + action: "fetch", + type: "blob", + csp: "default-src 'self' blob: ; connect-src http://example.com", + expectBlocked: true + }, + { + id: "worker_blob_xhr_cross_bad", + base: CROSS_BASE, + action: "xhr", + type: "blob", + csp: "default-src 'self' blob: ; connect-src http://example.com", + expectBlocked: true + }, + { + id: "worker_blob_importScripts_cross_bad", + base: CROSS_BASE, + action: "importScripts", + type: "blob", + csp: "default-src 'self' blob: ; connect-src http://example.com", + expectBlocked: true + }, + + // create parent worker from url, child worker from blob, + // Parent delivery csp then propagate to child + // csp should be: "default-src 'self' blob: ; connect-src 'self' http://example.com", + { + id: "worker_url_child_blob_fetch_same_good", + base: SAME_BASE, + action: "fetch", + child: "blob", + childCsp: "default-src 'none'", + type: "url", + csp: "default-src 'self' blob: ; connect-src 'self' http://example.com", + expectBlocked: false + }, + { + id: "worker_url_child_blob_importScripts_same_good", + base: SAME_BASE, + action: "importScripts", + child: "blob", + childCsp: "default-src 'none'", + type: "url", + csp: "default-src 'self' blob: ; connect-src 'self' http://example.com", + expectBlocked: false + }, + { + id: "worker_url_child_blob_xhr_same_good", + base: SAME_BASE, + child: "blob", + childCsp: "default-src 'none'", + action: "xhr", + type: "url", + csp: "default-src 'self' blob: ; connect-src 'self' http://example.com", + expectBlocked: false + }, + { + id: "worker_url_child_blob_fetch_cross_good", + base: CROSS_BASE, + action: "fetch", + child: "blob", + childCsp: "default-src 'none'", + type: "url", + csp: "default-src 'self' blob: ; connect-src 'self' http://example.com", + expectBlocked: false + }, + { + id: "worker_url_child_blob_importScripts_cross_bad", + base: CROSS_BASE, + action: "importScripts", + child: "blob", + childCsp: "default-src 'none'", + type: "url", + csp: "default-src 'self' blob: ; connect-src 'self' http://example.com", + expectBlocked: true + }, + { + id: "worker_url_child_blob_xhr_cross_godd", + base: CROSS_BASE, + child: "blob", + childCsp: "default-src 'none'", + action: "xhr", + type: "url", + csp: "default-src 'self' blob: ; connect-src 'self' http://example.com", + expectBlocked: false + }, + + + // create parent worker from blob, child worker from blob, + // Csp: document->parent->child + // csp should be : default-src 'self' blob: 'unsafe-inline' + { + id: "worker_blob_child_blob_fetch_same_good", + base: SAME_BASE, + child: "blob", + childCsp: "default-src 'none'", + action: "fetch", + type: "blob", + csp: "default-src 'self' blob:", + expectBlocked: false + }, + { + id: "worker_blob_child_blob_xhr_same_good", + base: SAME_BASE, + child: "blob", + childCsp: "default-src 'none'", + action: "xhr", + type: "blob", + csp: "default-src 'self' blob:", + expectBlocked: false + }, + { + id: "worker_blob_child_blob_importScripts_same_good", + base: SAME_BASE, + action: "importScripts", + child: "blob", + childCsp: "default-src 'none'", + type: "blob", + csp: "default-src 'self' blob:", + expectBlocked: false + }, + { + id: "worker_blob_child_blob_fetch_cross_bad", + base: CROSS_BASE, + child: "blob", + childCsp: "default-src 'none'", + action: "fetch", + type: "blob", + csp: "default-src 'self' blob:", + expectBlocked: true + }, + { + id: "worker_blob_child_blob_xhr_cross_bad", + base: CROSS_BASE, + child: "blob", + childCsp: "default-src 'none'", + action: "xhr", + type: "blob", + csp: "default-src 'self' blob:", + expectBlocked: true + }, + { + id: "worker_blob_child_blob_importScripts_cross_bad", + base: CROSS_BASE, + action: "importScripts", + child: "blob", + childCsp: "default-src 'none'", + type: "blob", + csp: "default-src 'self' blob:", + expectBlocked: true + }, + + // create parent worker from url, child worker from url, + // child delivery csp from header + // csp should be : default-src 'none' + { + id: "worker_url_child_url_fetch_cross_bad", + base: CROSS_BASE, + action: "fetch", + child: "url", + childCsp: "default-src 'none'", + type: "url", + csp: "default-src 'self' blob:", + expectBlocked: true + }, + { + id: "worker_url_child_url_xhr_cross_bad", + base: CROSS_BASE, + child: "url", + childCsp: "default-src 'none'", + action: "xhr", + type: "url", + csp: "default-src 'self' blob:", + expectBlocked: true + }, + { + id: "worker_url_child_url_importScripts_cross_bad", + base: CROSS_BASE, + action: "importScripts", + child: "url", + childCsp: "default-src 'none'", + type: "url", + csp: "default-src 'self' blob:", + expectBlocked: true + }, + { + id: "worker_url_child_url_fetch_same_bad", + base: SAME_BASE, + action: "fetch", + child: "url", + childCsp: "default-src 'none'", + type: "url", + csp: "default-src 'self' blob:", + expectBlocked: true + }, + { + id: "worker_url_child_url_xhr_same_bad", + base: SAME_BASE, + child: "url", + childCsp: "default-src 'none'", + action: "xhr", + type: "url", + csp: "default-src 'self' blob:", + expectBlocked: true + }, + { + id: "worker_url_child_url_importScripts_same_bad", + base: SAME_BASE, + action: "importScripts", + child: "url", + childCsp: "default-src 'none'", + type: "url", + csp: "default-src 'self' blob:", + expectBlocked: true + }, + + // create parent worker from blob, child worker from url, + // child delivery csp from header + // csp should be : default-src 'none' + { + id: "worker_blob_child_url_fetch_cross_bad", + base: CROSS_BASE, + child: "url", + childCsp: "default-src 'none'", + action: "fetch", + type: "blob", + csp: "default-src 'self' blob:", + expectBlocked: true + }, + { + id: "worker_blob_child_url_xhr_cross_bad", + base: CROSS_BASE, + child: "url", + childCsp: "default-src 'none'", + action: "xhr", + type: "blob", + csp: "default-src 'self' blob:", + expectBlocked: true + }, + { + id: "worker_blob_child_url_importScripts_cross_bad", + base: CROSS_BASE, + action: "importScripts", + child: "url", + childCsp: "default-src 'none'", + type: "blob", + csp: "default-src 'self' blob:", + expectBlocked: true + }, + { + id: "worker_blob_child_url_fetch_same_bad", + base: SAME_BASE, + child: "url", + childCsp: "default-src 'none'", + action: "fetch", + type: "blob", + csp: "default-src 'self' blob:", + expectBlocked: true + }, + { + id: "worker_blob_child_url_xhr_same_bad", + base: SAME_BASE, + child: "url", + childCsp: "default-src 'none'", + action: "xhr", + type: "blob", + csp: "default-src 'self' blob:", + expectBlocked: true + }, + { + id: "worker_blob_child_url_importScripts_same_bad", + base: SAME_BASE, + action: "importScripts", + child: "url", + childCsp: "default-src 'none'", + type: "blob", + csp: "default-src 'self' blob:", + expectBlocked: true + }, + + + ]; + + async function runWorkerTest(data) { + let src = SJS; + src += "?base=" + escape(data.base); + src += "&action=" + escape(data.action); + src += "&csp=" + escape(data.csp); + src += "&id=" + escape(data.id); + + if (data.child) { + src += "&child=" + escape(data.child); + } + + if (data.childCsp) { + src += "&childCsp=" + escape(data.childCsp); + } + + switch (data.type) { + case "url": + new Worker(src); + break; + + case "blob": + new Worker(URL.createObjectURL(await doXHRGetBlob(src))); + break; + + default: + throw "Unsupport type"; + } + + let checkUri = data.base + "?id=" + data.id; + await assertCSPBlock(checkUri, data.expectBlocked); + runNextTest(); + }; + + tests.forEach(function(test) { + addAsyncTest(async function() { + runWorkerTest(test); + }); + }); + + runNextTest(); + </script> + + </body> +</html> diff --git a/dom/security/test/csp/main_csp_worker.html^headers^ b/dom/security/test/csp/main_csp_worker.html^headers^ new file mode 100644 index 0000000000..4597e01040 --- /dev/null +++ b/dom/security/test/csp/main_csp_worker.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' blob: 'unsafe-inline' diff --git a/dom/security/test/csp/mochitest.ini b/dom/security/test/csp/mochitest.ini new file mode 100644 index 0000000000..552fdf90f2 --- /dev/null +++ b/dom/security/test/csp/mochitest.ini @@ -0,0 +1,473 @@ +[DEFAULT] +support-files = + file_base_uri_server.sjs + file_blob_data_schemes.html + file_blob_uri_blocks_modals.html + file_blob_uri_blocks_modals.html^headers^ + file_blob_top_nav_block_modals.html + file_blob_top_nav_block_modals.html^headers^ + file_connect-src.html + file_connect-src-fetch.html + file_CSP.css + file_CSP.sjs + file_dummy_pixel.png + 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_frameancestors_userpass.html + file_frameancestors_userpass_frame_a.html + file_frameancestors_userpass_frame_b.html + file_frameancestors_userpass_frame_c.html + file_frameancestors_userpass_frame_c.html^headers^ + file_frameancestors_userpass_frame_d.html + file_frameancestors_userpass_frame_d.html^headers^ + 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_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_bug1452037.html + file_bug1505412.sjs + file_bug1505412_reporter.sjs + file_bug1505412_frame.html + file_bug1505412_frame.html^headers^ + file_policyuri_regression_from_multipolicy.html + file_policyuri_regression_from_multipolicy.html^headers^ + file_policyuri_regression_from_multipolicy_policy + file_nonce_source.html + file_nonce_source.html^headers^ + file_nonce_redirects.html + file_nonce_redirector.sjs + file_bug941404.html + file_bug941404_xhr.html + file_bug941404_xhr.html^headers^ + file_frame_ancestors_ro.html + file_frame_ancestors_ro.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_empty_directive.html + file_empty_directive.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 + 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_cors.html + file_upgrade_insecure_cors_server.sjs + file_upgrade_insecure_loopback.html + file_upgrade_insecure_loopback_form.html + file_upgrade_insecure_loopback_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_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_marquee.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_upgrade_insecure_navigation.sjs + file_punycode_host_src.sjs + file_punycode_host_src.js + file_iframe_srcdoc.sjs + file_iframe_sandbox_srcdoc.html + file_iframe_sandbox_srcdoc.html^headers^ + file_websocket_self.html + file_websocket_csp_upgrade.html + file_websocket_explicit.html + file_websocket_self_wsh.py + file_win_open_blocked.html + file_image_nonce.html + file_image_nonce.html^headers^ + file_ignore_xfo.html + file_ignore_xfo.html^headers^ + file_ro_ignore_xfo.html + file_ro_ignore_xfo.html^headers^ + file_no_log_ignore_xfo.html + file_no_log_ignore_xfo.html^headers^ + file_data_csp_inheritance.html + file_data_csp_merge.html + file_data_doc_ignore_meta_csp.html + file_report_font_cache-1.html + file_report_font_cache-2.html + file_report_font_cache-2.html^headers^ + Ahem.ttf + file_independent_iframe_csp.html +prefs = + security.mixed_content.upgrade_display_content=false + javascript.options.experimental.shadow_realms=true + +[test_base-uri.html] +[test_blob_data_schemes.html] +[test_blob_uri_blocks_modals.html] +skip-if = xorigin && os == "linux" && (asan || tsan) # alert should be blocked by CSP - got false, expected true +[test_bug1777572.html] +support-files = file_bug1777572.html +skip-if = toolkit == 'android' # This unusual window.close/open test times out on Android. +[test_connect-src.html] +[test_CSP.html] +[test_bug1452037.html] +[test_allow_https_schemes.html] +[test_bug663567.html] +skip-if = + fission && xorigin && debug && os == "win" # Bug 1716406 - New fission platform triage +[test_bug802872.html] +[test_bug885433.html] +[test_bug888172.html] +[test_bug1505412.html] +skip-if = !debug +[test_evalscript.html] +[test_evalscript_blocked_by_strict_dynamic.html] +[test_evalscript_allowed_by_strict_dynamic.html] +[test_frameancestors.html] +skip-if = xorigin # JavaScript error: http://mochi.xorigin-test:8888/tests/SimpleTest/TestRunner.js, line 157: SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object +[test_frameancestors_userpass.html] +[test_inlinescript.html] +[test_inlinestyle.html] +[test_invalid_source_expression.html] +[test_bug836922_npolicies.html] +skip-if = verify +[test_bug886164.html] +[test_redirects.html] +[test_bug910139.html] +skip-if = verify +[test_bug909029.html] +[test_bug1229639.html] +[test_bug1579094.html] +[test_frame_ancestors_ro.html] +[test_policyuri_regression_from_multipolicy.html] +[test_nonce_source.html] +[test_nonce_redirects.html] +[test_bug941404.html] +[test_form-action.html] +[test_hash_source.html] +skip-if = + fission && xorigin && debug # Bug 1716406 - New fission platform triage +[test_scheme_relative_sources.html] +[test_ignore_unsafe_inline.html] +skip-if = xorigin # JavaScript error: http://mochi.xorigin-test:8888/tests/SimpleTest/TestRunner.js, line 157: SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object, [Child 3789, Main Thread] WARNING: NS_ENSURE_TRUE(request) failed: file /builds/worker/checkouts/gecko/netwerk/base/nsLoadGroup.cpp, line 591 +[test_self_none_as_hostname_confusion.html] +[test_empty_directive.html] +[test_path_matching.html] +[test_path_matching_redirect.html] +[test_report_uri_missing_in_report_only_header.html] +[test_report.html] +fail-if = xorigin +[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_dual_header.html] +[test_win_open_blocked.html] +[test_upgrade_insecure.html] +skip-if = + os == 'linux' && bits == 64 # Bug 1620516 + os == "android" # Bug 1777028 +[test_upgrade_insecure_reporting.html] +[test_upgrade_insecure_cors.html] +[test_upgrade_insecure_loopback.html] +[test_report_for_import.html] +fail-if = xorigin +[test_blocked_uri_in_reports.html] +[test_service_worker.html] +[test_child-src_worker.html] +[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] +skip-if = + fission && xorigin && debug && (os == "win" || os == "linux") # Bug 1716406 - New fission platform triage +[test_iframe_sandbox_top_1.html] +[test_sandbox.html] +skip-if = true # Bug 1657934 +[test_ping.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_upgrade_insecure_navigation.html] +[test_punycode_host_src.html] +[test_iframe_sandbox_srcdoc.html] +skip-if = + fission && xorigin && debug && os == "win" # Bug 1716406 - New fission platform triage +[test_iframe_srcdoc.html] +[test_image_nonce.html] +[test_websocket_self.html] +skip-if = toolkit == 'android' # no websocket support Bug 982828 +[test_websocket_localhost.html] +skip-if = toolkit == 'android' # no websocket support Bug 982828 +[test_ignore_xfo.html] +skip-if = xorigin # JavaScript error: http://mochi.xorigin-test:8888/tests/SimpleTest/TestRunner.js, line 157: SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object +[test_data_csp_inheritance.html] +[test_data_csp_merge.html] +[test_report_font_cache.html] +[test_data_doc_ignore_meta_csp.html] +[test_meta_csp_self.html] +[test_uir_top_nav.html] +support-files = + file_uir_top_nav.html + file_uir_top_nav_dummy.html +[test_sandbox_allow_scripts.html] +support-files = + file_sandbox_allow_scripts.html + file_sandbox_allow_scripts.html^headers^ +[test_worker_src.html] +support-files = + file_worker_src_worker_governs.html + file_worker_src_child_governs.html + file_worker_src_script_governs.html + file_worker_src.js + file_spawn_worker.js + file_spawn_shared_worker.js + file_spawn_service_worker.js +[test_frame_src.html] +support-files = + file_frame_src_frame_governs.html + file_frame_src_child_governs.html + file_frame_src.js + file_frame_src_inner.html +[test_security_policy_violation_event.html] +[test_csp_worker_inheritance.html] +support-files = + worker.sjs + worker_helper.js + main_csp_worker.html + main_csp_worker.html^headers^ +[test_nonce_snapshot.html] +support-files = + file_nonce_snapshot.sjs +[test_uir_windowwatcher.html] +support-files = + file_windowwatcher_frameA.html + file_windowwatcher_subframeB.html + file_windowwatcher_subframeC.html + file_windowwatcher_subframeD.html + file_windowwatcher_win_open.html +[test_script_template.html] +support-files = + file_script_template.html + file_script_template.js +[test_parent_location_js.html] +support-files = + file_parent_location_js.html + file_iframe_parent_location_js.html +[test_navigate_to.html] +support-files = + file_navigate_to.sjs + file_navigate_to_request.html +[test_independent_iframe_csp.html] +[test_xslt_inherits_csp.html] +support-files = + file_xslt_inherits_csp.xml + file_xslt_inherits_csp.xml^headers^ + file_xslt_inherits_csp.xsl +[test_object_inherit.html] +support-files = + file_object_inherit.html +[test_link_rel_preload.html] +support-files = + file_link_rel_preload.html +[test_image_document.html] +support-files = + file_image_document_pixel.png + file_image_document_pixel.png^headers^ +[test_svg_inline_style.html] +support-files = + file_svg_inline_style_base.html + file_svg_inline_style_csp.html + file_svg_inline_style_server.sjs +[test_upgrade_insecure_navigation_redirect.html] +support-files = + file_upgrade_insecure_navigation_redirect.sjs + file_upgrade_insecure_navigation_redirect_same_origin.html + file_upgrade_insecure_navigation_redirect_cross_origin.html +[test_csp_style_src_empty_hash.html] +[test_csp_frame_ancestors_about_blank.html] +support-files = + file_csp_frame_ancestors_about_blank.html + file_csp_frame_ancestors_about_blank.html^headers^ +[test_blocked_uri_redirect_frame_src.html] +support-files = + file_blocked_uri_redirect_frame_src.html + file_blocked_uri_redirect_frame_src.html^headers^ + file_blocked_uri_redirect_frame_src_server.sjs +[test_blocked_uri_in_violation_event_after_redirects.html] +support-files = + file_blocked_uri_in_violation_event_after_redirects.html + file_blocked_uri_in_violation_event_after_redirects.sjs +[test_bug1738418.html] +support-files = + file_bug1738418_parent.html + file_bug1738418_parent.html^headers^ + file_bug1738418_child.html diff --git a/dom/security/test/csp/referrerdirective.sjs b/dom/security/test/csp/referrerdirective.sjs new file mode 100644 index 0000000000..071def0cdb --- /dev/null +++ b/dom/security/test/csp/referrerdirective.sjs @@ -0,0 +1,40 @@ +// 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 0000000000..0aaed5bcf2 --- /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 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"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} + +examiner.prototype = { + observe(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() { + 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 0000000000..330c1a64e9 --- /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 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"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} + +examiner.prototype = { + observe(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() { + 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 0000000000..ecff523967 --- /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 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"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} + +examiner.prototype = { + observe(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() { + 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 0000000000..40ebd592b3 --- /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 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"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} + +examiner.prototype = { + observe(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() { + 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 0000000000..babb9db9bc --- /dev/null +++ b/dom/security/test/csp/test_CSP.html @@ -0,0 +1,130 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Content Security Policy Connections</title> + <script 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, + media_good: -1, + media_bad: -1, + font_good: -1, + font_bad: -1, + object_good: -1, + object_bad: -1, +}; + +SpecialPowers.registerObservers("csp-on-violate-policy"); + +// 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"); + SpecialPowers.addObserver(this, "specialpowers-csp-on-violate-policy"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(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" || + topic === "specialpowers-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() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-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':[// On a cellular connection the default preload value is 0 ("preload + // none"). Our Android emulators emulate a cellular connection, and + // so by default preload no media data. This causes the media_* tests + // to timeout. We set the default used by cellular connections to the + // same as used by non-cellular connections in order to get + // consistent behavior across platforms/devices. + ["media.preload.default", 2], + ["media.preload.default.cellular", 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 0000000000..be1f030fb9 --- /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 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); + document.getElementById("testframe").src = src; + } +} + +function test() { + try { + document.getElementById("testframe").removeEventListener('load', test); + 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 0000000000..4d5c5504af --- /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 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); + 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); +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 0000000000..37a22db050 --- /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 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 allowlist + * 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); + +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); + 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_blob_uri_blocks_modals.html b/dom/security/test/csp/test_blob_uri_blocks_modals.html new file mode 100644 index 0000000000..3f8de94ed2 --- /dev/null +++ b/dom/security/test/csp/test_blob_uri_blocks_modals.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1432170 - Block alert box and new window open as per the sandbox + allow-scripts CSP</title> + <script type="application/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> + +/* Description of the test: + * We apply the sanbox allow-scripts CSP to the blob iframe and check + * if the alert box and new window open is blocked correctly by the CSP. + */ +var testsToRun = { + block_window_open_test: false, + block_alert_test: false, + block_top_nav_alert_test: false, +}; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("have to test that alert dialogue is blocked"); + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + switch (event.data.test) { + case "block_window_open_test": + testsToRun.block_window_open_test = true; + break; + case "block_alert_test": + is(event.data.msg, "alert blocked by CSP", "alert blocked by CSP"); + testsToRun.block_alert_test = true; + break; + case "block_top_nav_alert_test": + testsToRun.block_top_nav_alert_test = true; + break; + } +} + +var w; +// Set "privacy.partition.bloburl_per_agent_cluster" to false until Bug 1667348 is fixed. +SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.bloburl_per_agent_cluster", false]]}, () => { + // start the test + document.getElementById("testframe").src = "file_blob_uri_blocks_modals.html"; + w = window.open("file_blob_top_nav_block_modals.html"); +}); + +// If alert window is not blocked by CSP then event message is not recieved and +// test fails after setTimeout interval of 1 second. +setTimeout(function () { + is(testsToRun.block_top_nav_alert_test, true, + "blob top nav alert should be blocked by CSP"); + testsToRun.block_top_nav_alert_test = true; + is(testsToRun.block_alert_test, true, + "alert should be blocked by CSP"); + testsToRun.block_alert_test = true; + checkTestsCompleted(); + },1000); + +function checkTestsCompleted() { + for (var prop in testsToRun) { + // some test hasn't run yet so we're not done + if (!testsToRun[prop]) { + return; + } + } + window.removeEventListener("message", receiveMessage); + w.close(); + SimpleTest.finish(); +} + +</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 0000000000..d60f904b6c --- /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 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); +function receiveMessage(event) { + checkResults(event.data.result); +} + +function loadNextTest() { + counter++; + if (counter == tests.length) { + window.removeEventListener("message", receiveMessage); + 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 0000000000..b32c1fccd5 --- /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 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); + SimpleTest.finish(); +} + +window.addEventListener("message", receiveMessage); +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 0000000000..f40d98efc5 --- /dev/null +++ b/dom/security/test/csp/test_blocked_uri_in_reports.html @@ -0,0 +1,80 @@ +<!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 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 the original URI: + * http://example.com/tests/dom/security/test/csp/file_path_matching_redirect_server.sjs + * instead of the redirected URI: + * 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://example.com/tests/dom/security/test/csp/file_path_matching_redirect_server.sjs", "Incorrect blocked-uri"); + } catch (e) { + ok(false, "Could not query report (exception: " + e + ")"); + } + } + + script.removeMessageListener('opening-request-completed', ml); + script.sendAsyncMessage("finish"); + 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_blocked_uri_in_violation_event_after_redirects.html b/dom/security/test/csp/test_blocked_uri_in_violation_event_after_redirects.html new file mode 100644 index 0000000000..6965cbeb92 --- /dev/null +++ b/dom/security/test/csp/test_blocked_uri_in_violation_event_after_redirects.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1542194 - Check blockedURI in violation reports after redirects</title> + <script 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"> + +SimpleTest.waitForExplicitFinish(); + +let seenViolations = 0; +let expectedViolations = 3; + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + + seenViolations++; + + let blockedURI = event.data.blockedURI; + + if (blockedURI.includes("test1")) { + is(blockedURI, + "http://example.com/tests/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.sjs?test1a", + "Test 1 should be the URI before redirect"); + } else if (blockedURI.includes("test2")) { + is(blockedURI, + "http://test2.example.com", + "Test 2 should be the redirected pre-path URI"); + } else if (blockedURI.includes("test3")) { + is(blockedURI, + "http://test3.example.com", + "Test 3 should be the redirected pre-path URI"); + } else { + ok(false, "sanity: how can we end up here?"); + } + + if (seenViolations < expectedViolations) { + return; + } + + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +let testframe = document.getElementById("testframe"); +// This has to be same-origin with the test1 URL. +testframe.src = "http://example.com/tests/dom/security/test/csp/file_blocked_uri_in_violation_event_after_redirects.html"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_blocked_uri_redirect_frame_src.html b/dom/security/test/csp/test_blocked_uri_redirect_frame_src.html new file mode 100644 index 0000000000..a946718bc2 --- /dev/null +++ b/dom/security/test/csp/test_blocked_uri_redirect_frame_src.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1687342 - Check blocked-uri in csp-reports after frame redirect</title> + <script 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: + * We load a document from http://mochi.test with a CSP of `frame-src example.com`. + * We then load an iframe from example.com which redirects to test1.example.com and + * ensure that the report-uri is the origin of the frame before the blocked redirect. + */ + +SimpleTest.waitForExplicitFinish(); + +const BLOCKED_URI = "http://example.com"; + +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 + ")"); + return; + } + try { + var reportObj = JSON.parse(msg.report); + } catch (e) { + ok(false, "Could not parse JSON (exception: " + e + ")"); + } + try { + var cspReport = reportObj["csp-report"]; + is(cspReport["blocked-uri"], BLOCKED_URI, "Incorrect blocked-uri"); + } catch (e) { + ok(false, "Could not query report (exception: " + e + ")"); + } + + script.removeMessageListener('opening-request-completed', ml); + script.sendAsyncMessage("finish"); + SimpleTest.finish(); +}); + +SimpleTest.waitForExplicitFinish(); + +function runTest() { + let testframe = document.getElementById("testframe"); + testframe.src = "file_blocked_uri_redirect_frame_src.html"; +} + +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 0000000000..e224fe1ffb --- /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 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"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} + +examiner.prototype = { + observe(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() { + 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 0000000000..14e8f74baa --- /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 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 0000000000..b06b08d092 --- /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 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_bug1388015.html b/dom/security/test/csp/test_bug1388015.html new file mode 100644 index 0000000000..5ca0605688 --- /dev/null +++ b/dom/security/test/csp/test_bug1388015.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Bug 1388015 - Test if Firefox respect Port in Wildcard Host </title> + <meta http-equiv="Content-Security-Policy" content="img-src https://*:443"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + + <img alt="Should be Blocked"> + <script class="testbody" type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + + let image = document.querySelector("img"); + + Promise.race([ + new Promise((res) => { + window.addEventListener("securitypolicyviolation", () => res(true), {once:true}); + }), + new Promise((res) => { + image.addEventListener("load", () => res(false),{once:true}); + })]) + .then((result) => { + ok(result, " CSP did block Image with wildcard and mismatched Port"); + }) + .then(()=> Promise.race([ + new Promise((res) => { + window.addEventListener("securitypolicyviolation", () => res(false), {once:true}); + }), + new Promise((res) => { + image.addEventListener("load", () => res(true),{once:true}); + requestIdleCallback(()=>{ + image.src = "https://example.com:443/tests/dom/security/test/csp/file_dummy_pixel.png" + }) + })])) + .then((result) => { + ok(result, " CSP did load the Image with wildcard and matching Port"); + SimpleTest.finish(); + }) + image.src = "file_dummy_pixel.png" // mochi.test:8888 + </script> +</body> +</html> diff --git a/dom/security/test/csp/test_bug1452037.html b/dom/security/test/csp/test_bug1452037.html new file mode 100644 index 0000000000..fa46e91291 --- /dev/null +++ b/dom/security/test/csp/test_bug1452037.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test if "script-src: sha-... " Allowlists "javascript:" URIs</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <iframe></iframe> + +<script class="testbody"> + SimpleTest.requestCompleteLog(); + SimpleTest.waitForExplicitFinish(); + + let frame = document.querySelector("iframe"); + + window.addEventListener("message", (msg) => { + ok(false, "The CSP did not block javascript:uri"); + SimpleTest.finish(); + }); + + document.addEventListener("securitypolicyviolation", () => { + ok(true, "The CSP did block javascript:uri"); + SimpleTest.finish(); + }); + + frame.addEventListener("load", () => { + let link = frame.contentWindow.document.querySelector("a"); + frame.contentWindow.document.addEventListener("securitypolicyviolation", () => { + ok(true, "The CSP did block javascript:uri"); + SimpleTest.finish(); + }) + link.click(); + }); + frame.src = "file_bug1452037.html"; + + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_bug1505412.html b/dom/security/test/csp/test_bug1505412.html new file mode 100644 index 0000000000..717af2054b --- /dev/null +++ b/dom/security/test/csp/test_bug1505412.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title> Bug 1505412 CSP-RO reports violations in inline-scripts with nonce</title> + <script src="/tests/SimpleTest/SimpleTest.js" nonce="foobar"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + + +<body> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1505412">Test for 1505412 </a> + <script class="testbody" type="text/javascript" nonce="foobar"> + /* Description of the test: + 1: We setup a Proxy that will cause the Test to Fail + if Firefox sends a CSP-Report to /report + 2: We Load an iframe with has a Script pointing to + file_bug1505412.sjs + 3: The Preloader will fetch the file and Gets redirected + 4: If correct, the File should be loaded and no CSP-Report + should be send. + */ + + + + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestCompleteLog(); + SimpleTest.requestLongerTimeout(2); // Or might fail for Linux-Debug in some cases. + var script; + + window.addEventListener("load",()=>{ + let t = document.querySelector("#target"); + t.src = "file_bug1505412_frame.html"; + t.addEventListener("load",async () => { + let reportCount = await fetch("file_bug1505412_reporter.sjs?state").then(r => r.text()); + info(reportCount); + ok(reportCount == 0 , "Script Loaded without CSP beeing triggered"); + await fetch("file_bug1505412_reporter.sjs?flush"); + SimpleTest.finish(); + }); + }) + + </script> + <iframe id="target" frameborder="0"></iframe> +</body> + +</html>
\ No newline at end of file diff --git a/dom/security/test/csp/test_bug1579094.html b/dom/security/test/csp/test_bug1579094.html new file mode 100644 index 0000000000..b3568586d4 --- /dev/null +++ b/dom/security/test/csp/test_bug1579094.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test if Wildcard CSP supports ExternalProtocol</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta meta http-equiv="Content-security-policy" content="frame-src SomeExternalProto://*"> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> +<script class="testbody" type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + + document.addEventListener("securitypolicyviolation",()=>{ + ok(false, "Error: ExternalProtocol Was blocked"); + SimpleTest.finish(); + }); + + window.addEventListener("load", ()=>{ + ok(true, "Error: ExternalProtocol was passed"); + SimpleTest.finish(); + }); +</script> + +<iframe src="SomeExternalProto:foo@bar.com"> + + +</body> +</html> diff --git a/dom/security/test/csp/test_bug1738418.html b/dom/security/test/csp/test_bug1738418.html new file mode 100644 index 0000000000..9fdc723b80 --- /dev/null +++ b/dom/security/test/csp/test_bug1738418.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1738418: CSP sandbox for embed/object frames</title> + <script 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"> + +SimpleTest.waitForExplicitFinish(); + +var elements = new Set(["iframe", "embed", "object"]); + +window.addEventListener("message", event => { + is(event.data.domain, "", `document in <${event.data.element}> should have sandboxed origin`); + elements.delete(event.data.element); + if (elements.size == 0) { + SimpleTest.finish(); + } +}); + +document.getElementById("testframe").src = "file_bug1738418_parent.html"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_bug1777572.html b/dom/security/test/csp/test_bug1777572.html new file mode 100644 index 0000000000..f735f4fb6a --- /dev/null +++ b/dom/security/test/csp/test_bug1777572.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>bug 1777572</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + + async function testCSPInheritance(closeOpenerWindow) { + let url = "file_bug1777572.html"; + if (closeOpenerWindow) { + url += "?close"; + } + let win = window.open(url); + return new Promise((resolve) => { + window.addEventListener("message", (event) => { + ok(event.data.includes("img-src"), "Got expected data " + event.data); + resolve(); + }, { once: true}); + }); + } + + async function run() { + // Test that CSP inheritance to the initial about:blank works the same way + // whether or not the opener is already closed when window.open is called. + await testCSPInheritance(false); + await testCSPInheritance(true); + SimpleTest.finish(); + } + + </script> +</head> +<body onload="run()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</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 0000000000..137d459654 --- /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 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); +document.getElementById('xsltframe').src = 'file_bug663567_allows.xml'; + +document.getElementById('xsltframe2').addEventListener('load', checkBlocked); +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 0000000000..956159ddcc --- /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 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 allowlisted domain!"); + checkExplicitFinish(); +}, false); +addEventListener('allowedEventSrcCallbackFailed', function (e) { + ok(false, "Error: CSP blocks EventSource for allowlisted domain!"); + checkExplicitFinish(); +}, false); + +// add event listeners for CSP-blocked EventSrc callbacks +addEventListener('blockedEventSrcCallbackOK', function (e) { + ok(false, "Error: CSP allows EventSource to not allowlisted domain!"); + checkExplicitFinish(); +}, false); +addEventListener('blockedEventSrcCallbackFailed', function (e) { + ok(true, "OK: CSP blocks EventSource for not allowlisted 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 0000000000..e418969e3d --- /dev/null +++ b/dom/security/test/csp/test_bug836922_npolicies.html @@ -0,0 +1,235 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Content Security Policy multiple policy support (regular and Report-Only mode)</title> + <script 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"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(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() { + 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() { + 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() { + 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() { + 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(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 0000000000..c7c17d25b6 --- /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 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); +} + +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); +</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 0000000000..5347d42ed8 --- /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 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); + +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"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(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() { + 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 0000000000..a78258e21f --- /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 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); +} + +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); +} + +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); +</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 0000000000..7a3ac81a1b --- /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 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"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(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() { + 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); +document.getElementById('testframe2').src = 'file_bug909029_none.html'; +document.getElementById('testframe2').addEventListener('load', checkInlineWithNone); + </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 0000000000..bbebedf877 --- /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 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); + 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); +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 0000000000..7c35c38aa1 --- /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 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"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} + +examiner.prototype = { + observe(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() { + 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 0000000000..2b85e280bd --- /dev/null +++ b/dom/security/test/csp/test_child-src_iframe.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1045891</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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: 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); + +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 0000000000..a1b1c9c2b4 --- /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 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); + + 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 0000000000..154e533551 --- /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 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 https://www.example.org 'unsafe-inline'" + }, + 'script-src-service_worker': { + id: "script-src-service_worker", + file: SERVICE_WORKER_TEST_FILE, + result : "blocked", + policy : "default-src 'none'; script-src https://www.example.org '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 https://www.example.org '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); + + 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 0000000000..9d36e73e0c --- /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 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); + + 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 0000000000..1ae4482dd8 --- /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 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"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(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() { + 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_csp_frame_ancestors_about_blank.html b/dom/security/test/csp/test_csp_frame_ancestors_about_blank.html new file mode 100644 index 0000000000..8f57d9e133 --- /dev/null +++ b/dom/security/test/csp/test_csp_frame_ancestors_about_blank.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1668071 - CSP frame-ancestors in about:blank</title> + <script 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"> + +/* Description of the test: + * We dynamically load an about:blank iframe which then loads a testframe + * including a CSP frame-ancestors directive which matches the including + * security context. We make sure that we not incorrectly block on + * about:blank which should inherit the security context. + */ + +SimpleTest.waitForExplicitFinish(); + +let aboutBlankFrame = document.createElement("iframe"); +document.body.appendChild(aboutBlankFrame); + +aboutBlankFrame.onload = function() { + ok(true, "aboutBlankFrame onload should fire"); + let aboutBlankDoc = aboutBlankFrame.contentDocument; + is(aboutBlankDoc.documentURI, "about:blank", + "sanity: aboutBlankFrame URI should be about:blank"); + + let testframe = aboutBlankDoc.createElement("iframe"); + aboutBlankDoc.body.appendChild(testframe); + testframe.onload = function() { + ok(true, "testframe onload should fire"); + let testDoc = SpecialPowers.wrap(testframe.contentDocument); + ok(testDoc.documentURI.endsWith("file_csp_frame_ancestors_about_blank.html"), + "sanity: document in testframe should be the testfile"); + + let cspJSON = testDoc.cspJSON; + ok(cspJSON.includes("frame-ancestors"), "found frame-ancestors directive"); + ok(cspJSON.includes("http://mochi.test:8888"), "found frame-ancestors value"); + + SimpleTest.finish(); + } + + testframe.onerror = function() { + ok(false, "testframe onerror should not fire"); + } + testframe.src = "file_csp_frame_ancestors_about_blank.html"; +} + +aboutBlankFrame.onerror = function() { + ok(false, "aboutBlankFrame onerror should not be called"); +} +aboutBlankFrame.src = "about:blank"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_csp_style_src_empty_hash.html b/dom/security/test/csp/test_csp_style_src_empty_hash.html new file mode 100644 index 0000000000..b500c196e6 --- /dev/null +++ b/dom/security/test/csp/test_csp_style_src_empty_hash.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1609122 - Empty Style Element with valid style-src hash </title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + + <meta http-equiv="Content-Security-Policy" content="style-src 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=';"> +</head> +<body> +<iframe style="width:100%;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +/* Description of the test: + * We try to load a stylesheet that is empty with a matching hash + * sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=' should match "" + * thus allow the style to be accessed + */ + + +var s = document.head.appendChild(document.createElement("style")); +s.disabled = true; + +is(s.disabled, true, "Empty Stylesheet with matching Hash was not blocked"); +SimpleTest.finish(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_csp_worker_inheritance.html b/dom/security/test/csp/test_csp_worker_inheritance.html new file mode 100644 index 0000000000..ebf6bea8a6 --- /dev/null +++ b/dom/security/test/csp/test_csp_worker_inheritance.html @@ -0,0 +1,20 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +<html> + <head> + <title>Test for Bug 1475849</title> + </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"> + document.getElementById('cspframe').src = 'main_csp_worker.html'; + </script> + + </body> +</html> diff --git a/dom/security/test/csp/test_data_csp_inheritance.html b/dom/security/test/csp/test_data_csp_inheritance.html new file mode 100644 index 0000000000..dd7f3174a2 --- /dev/null +++ b/dom/security/test/csp/test_data_csp_inheritance.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1381761 - Treating 'data:' documents as unique, opaque origins should still inherit the CSP</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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 an iframe using a meta CSP which includes another iframe + * using a data: URI. We make sure the CSP gets inherited into + * the data: URI iframe. + */ + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + // toplevel CSP should apply to data: URI iframe hence resulting + // in 1 applied policy. + is(event.data.result, 1, + "data: URI iframe inherits CSP from including context"); + SimpleTest.finish(); +} + +document.getElementById("testframe").src = "file_data_csp_inheritance.html"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_data_csp_merge.html b/dom/security/test/csp/test_data_csp_merge.html new file mode 100644 index 0000000000..87219c406d --- /dev/null +++ b/dom/security/test/csp/test_data_csp_merge.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1386183 - Meta CSP on data: URI iframe should be merged with toplevel CSP</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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 an iframe using a meta CSP which includes another iframe + * using a data: URI which also defines a meta CSP. We make sure the + * CSP from the including document gets merged with the data: URI + * CSP and applies to the data: URI iframe. + */ + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + // toplevel CSP + data: URI iframe meta CSP => 2 CSP policies + is(event.data.result, 2, + "CSP on data: URI iframe gets merged with CSP from including context"); + SimpleTest.finish(); +} + +document.getElementById("testframe").src = "file_data_csp_merge.html"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_data_doc_ignore_meta_csp.html b/dom/security/test/csp/test_data_doc_ignore_meta_csp.html new file mode 100644 index 0000000000..6f0a3fbbf6 --- /dev/null +++ b/dom/security/test/csp/test_data_doc_ignore_meta_csp.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1382869: data document should ignore meta csp</title> + <script 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 an iframe creating a new data document which defines + * a meta csp. We make sure the meta CSP is ignored and does not + * apply to the actual iframe document. + */ + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + is(event.data.result, "dataDocCreated", "sanity: received msg from loaded frame"); + + var frame = document.getElementById("testframe"); + var contentDoc = SpecialPowers.wrap(frame).contentDocument; + var cspOBJ = JSON.parse(contentDoc.cspJSON); + // make sure we got no policy attached + var policies = cspOBJ["csp-policies"]; + is(policies.length, 0, "there should be no CSP attached to the iframe"); + SimpleTest.finish(); +} + +document.getElementById("testframe").src = "file_data_doc_ignore_meta_csp.html"; + +</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 0000000000..776f1bb32f --- /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 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); + + // stylesheet: default background color within FF is transparent + var bgcolor = window.getComputedStyle(writemetacspframe.contentDocument.body) + .getPropertyValue("background-color"); + is(bgcolor, "rgba(0, 0, 0, 0)", "inital background value in FF should be 'transparent'"); + + // image: make sure image is blocked + var img = writemetacspframe.contentDocument.getElementById("testimage"); + is(img.naturalWidth, 0, "image width should be 0"); + is(img.naturalHeight, 0, "image height 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); + + // 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); + +// commenting out a meta CSP should result in loaded image, script, style +commentmetacspframe.src = 'file_doccomment_meta.html'; +commentmetacspframe.addEventListener('load', checkResultsAllowed); + +</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 0000000000..cfea86103b --- /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 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); + 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); + document.getElementById("testframe").src = src; +} + +SimpleTest.waitForExplicitFinish(); +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_empty_directive.html b/dom/security/test/csp/test_empty_directive.html new file mode 100644 index 0000000000..5957f99062 --- /dev/null +++ b/dom/security/test/csp/test_empty_directive.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1439425 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1439425</title> + <script 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=1439425">Mozilla Bug 1439425</a> +<p id="display"></p> + +<iframe id="cspframe"></iframe> + +<pre id="test"> + +<script class="testbody" type="text/javascript"> +let consoleCount = 0; + +function cleanup() { + SpecialPowers.postConsoleSentinel(); +} + +function finish() { + SimpleTest.finish(); +} + +SpecialPowers.registerConsoleListener(function ConsoleMsgListener(aMsg) { + if (aMsg.message == "SENTINEL") { + is(consoleCount, 0); + SimpleTest.executeSoon(finish); + } else if (aMsg.message.includes("Content Security Policy")) { + ++consoleCount; + ok(false, "Must never see a console warning here"); + } +}); + +// set up and start testing +SimpleTest.waitForExplicitFinish(); +let frame = document.getElementById('cspframe'); +frame.onload = () => { + SimpleTest.executeSoon(cleanup); +}; +frame.src = 'file_empty_directive.html'; +</script> +</pre> +</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 0000000000..bf1621f81e --- /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 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 = 19; + +// 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_evalscript_allowed_by_strict_dynamic.html b/dom/security/test/csp/test_evalscript_allowed_by_strict_dynamic.html new file mode 100644 index 0000000000..29c3e87287 --- /dev/null +++ b/dom/security/test/csp/test_evalscript_allowed_by_strict_dynamic.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" + content="script-src 'nonce-foobar' 'strict-dynamic' 'unsafe-eval'"> + <title>Bug 1439330 - CSP: eval is not blocked if 'strict-dynamic' is enabled + </title> + <script nonce="foobar" type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script nonce="foobar"> + +/* Description of the test: + * We apply the script-src 'nonce-foobar' 'strict-dynamic' 'unsafe-eval' CSP and + * check if the eval function is allowed correctly by the CSP. + */ + +SimpleTest.waitForExplicitFinish(); + +// start the test +try { + eval("1"); + ok(true, "eval allowed by CSP"); +} +catch (ex) { + ok(false, "eval should be allowed by CSP"); +} + +SimpleTest.finish(); + +</script> +</body> +</html>
\ No newline at end of file diff --git a/dom/security/test/csp/test_evalscript_blocked_by_strict_dynamic.html b/dom/security/test/csp/test_evalscript_blocked_by_strict_dynamic.html new file mode 100644 index 0000000000..179e865459 --- /dev/null +++ b/dom/security/test/csp/test_evalscript_blocked_by_strict_dynamic.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta http-equiv="Content-Security-Policy" + content="script-src 'nonce-foobar' 'strict-dynamic'"> + <title>Bug 1439330 - CSP: eval is not blocked if 'strict-dynamic' is enabled + </title> + <script nonce="foobar" type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script nonce="foobar"> + +/* Description of the test: + * We apply the script-src 'nonce-foobar' 'strict-dynamic' CSP and + * check if the eval function is blocked correctly by the CSP. + */ + +SimpleTest.waitForExplicitFinish(); + +// start the test +try { + eval("1"); + ok(false, "eval should be blocked by CSP"); +} +catch (ex) { + ok(true, "eval blocked by CSP"); +} + +SimpleTest.finish(); + +</script> +</body> +</html>
\ No newline at end of file diff --git a/dom/security/test/csp/test_fontloader.html b/dom/security/test/csp/test_fontloader.html new file mode 100644 index 0000000000..2f68223af1 --- /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 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"); + +async function checkResult() { + testframe.removeEventListener('load', checkResult); + try { + ok(compareSnapshots(await snapshotWindow(baselineframe.contentWindow), + await 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); + 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); + loadNextTest(); +} + +// make sure the main page is loaded before we start the test +function setupTests() { + baselineframe.addEventListener("load", startTests); + 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 0000000000..7bbc52a116 --- /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 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"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(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() { + 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 0000000000..f835504ff4 --- /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 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); + 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); + + // 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); + + // 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); +testframe.src = "file_form_action_server.sjs?loadframe"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_frame_ancestors_ro.html b/dom/security/test/csp/test_frame_ancestors_ro.html new file mode 100644 index 0000000000..1cfe6be1cd --- /dev/null +++ b/dom/security/test/csp/test_frame_ancestors_ro.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for frame-ancestors support in Content-Security-Policy-Report-Only</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width: 100%" id="cspframe"></iframe> +<script type="text/javascript"> +const docUri = "http://mochi.test:8888/tests/dom/security/test/csp/file_frame_ancestors_ro.html"; +const frame = document.getElementById("cspframe"); + +let testResults = { + reportFired: false, + frameLoaded: false +}; + +function checkResults(reportObj) { + let cspReport = reportObj["csp-report"]; + is(cspReport["document-uri"], docUri, "Incorrect document-uri"); + + // we can not test for the whole referrer since it includes platform specific information + is(cspReport.referrer, document.location.toString(), "Incorrect referrer"); + is(cspReport["blocked-uri"], document.location.toString(), "Incorrect blocked-uri"); + is(cspReport["violated-directive"], "frame-ancestors", "Incorrect violated-directive"); + is(cspReport["original-policy"], "frame-ancestors 'none'; report-uri http://mochi.test:8888/foo.sjs", "Incorrect original-policy"); + testResults.reportFired = true; +} + +let chromeScriptUrl = SimpleTest.getTestFileURL("file_report_chromescript.js"); +let 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 { + let reportObj = JSON.parse(msg.report); + // test for the proper values in the report object + checkResults(reportObj); + } catch (e) { + ok(false, "Error verifying report object (exception: " + e + ")"); + } + } + + script.removeMessageListener('opening-request-completed', ml); + script.sendAsyncMessage("finish"); + checkTestResults(); +}); + +frame.addEventListener( 'load', () => { + // Make sure the frame is still loaded + testResults.frameLoaded = true; + checkTestResults() +} ); + +function checkTestResults() { + if( testResults.reportFired && testResults.frameLoaded ) { + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); +frame.src = docUri; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_frame_src.html b/dom/security/test/csp/test_frame_src.html new file mode 100644 index 0000000000..f87549b72b --- /dev/null +++ b/dom/security/test/csp/test_frame_src.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1302667 - Test frame-src</title> + <script 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 inlcuding a frame a CSP of: + * >> frame-src https://example.com; child-src 'none' + * and make sure that frame-src governs frames correctly. In addition, + * we make sure that child-src is discarded in case frame-src is specified. + */ + +const ORIGIN_1 = "https://example.com/tests/dom/security/test/csp/"; +const ORIGIN_2 = "https://test1.example.com/tests/dom/security/test/csp/"; + +let TESTS = [ + // frame-src tests + ORIGIN_1 + "file_frame_src_frame_governs.html", + ORIGIN_2 + "file_frame_src_frame_governs.html", + // child-src tests + ORIGIN_1 + "file_frame_src_child_governs.html", + ORIGIN_2 + "file_frame_src_child_governs.html", +]; + +let testIndex = 0; + +function checkFinish() { + if (testIndex >= TESTS.length) { + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); + return; + } + runNextTest(); +} + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + let href = event.data.href; + let result = event.data.result; + + if (href.startsWith("https://example.com")) { + if (result == "frame-allowed") { + ok(true, "allowing frame from https://example.com (" + result + ")"); + } + else { + ok(false, "blocking frame from https://example.com (" + result + ")"); + } + } + else if (href.startsWith("https://test1.example.com")) { + if (result == "frame-blocked") { + ok(true, "blocking frame from https://test1.example.com (" + result + ")"); + } + else { + ok(false, "allowing frame from https://test1.example.com (" + result + ")"); + } + } + else { + // sanity check, we should never enter that branch, bust just in case... + ok(false, "unexpected result: " + result); + } + checkFinish(); +} + +function runNextTest() { + document.getElementById("testframe").src = TESTS[testIndex]; + testIndex++; +} + +// fire up the tests +runNextTest(); + +</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 0000000000..8b44ba72fb --- /dev/null +++ b/dom/security/test/csp/test_frameancestors.html @@ -0,0 +1,160 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Content Security Policy Frame Ancestors directive</title> + <script 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; + +// CSP frame-ancestor checks happen in the parent, hence we have to +// proxy the csp violation notifications. +SpecialPowers.registerObservers("csp-on-violate-policy"); + +// 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, "specialpowers-csp-on-violate-policy"); +} +examiner.prototype = { + observe(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 === "specialpowers-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() { + SpecialPowers.removeObserver(this, "specialpowers-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); + +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_frameancestors_userpass.html b/dom/security/test/csp/test_frameancestors_userpass.html new file mode 100644 index 0000000000..332318fe17 --- /dev/null +++ b/dom/security/test/csp/test_frameancestors_userpass.html @@ -0,0 +1,148 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Userpass in Frame Ancestors directive</title> + <script 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 = { + frame_a: -1, /* frame a allowed */ + frame_b: -1, /* frame b allowed */ +}; + +// Number of tests that pass for this file should be 1 +var expectedViolationsLeft = 1; + +// CSP frame-ancestor checks happen in the parent, hence we have to +// proxy the csp violation notifications. +SpecialPowers.registerObservers("csp-on-violate-policy"); + +// 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, "specialpowers-csp-on-violate-policy"); +} +examiner.prototype = { + observe(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_userpass.html")) { + return; + } + } catch (ex) { + // was not an nsIURI, so it was probably a cross-origin report. + } + + if (topic === "specialpowers-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() { + SpecialPowers.removeObserver(this, "specialpowers-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 { + //Check if @ symbol is there in URI. + if (uri.includes('@')) { + ok(false, ' URI contains userpass. Fetched URI is ' + uri); + } else { + framesThatShouldLoad[testname] = true; + ok(true, ' URI doesn\'t contain userpass. Fetched URI is ' + 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) { + + //Check if @ symbol is there in URI or in csp policy. + // Bug 1557712: Intermittent failure -> not sure why the 'uri' might ever + // be non existing at this point, however if there is no uri, there can + // also be no userpass! + if (policy.includes('@') || + (typeof uri === 'string' && uri.includes('@'))) { + ok(false, ' a CSP policy blocked frame from being loaded. But contains' + + ' userpass. Policy is: ' + policy + ';URI is: ' + uri ); + } else { + ok(true, ' a CSP policy blocked frame from being loaded. Doesn\'t contain'+ + ' userpass. Policy is: ' + policy + ';URI is: ' + uri ); + } + 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); + +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_userpass.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 0000000000..2334ae0101 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <script 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)).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); +</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 0000000000..cd7417bc8b --- /dev/null +++ b/dom/security/test/csp/test_iframe_sandbox.html @@ -0,0 +1,240 @@ +<!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 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.includes(a)) + 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); + + 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 0000000000..9c36aa5447 --- /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 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"); +} + +examiner.prototype = { + observe(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() { + 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 0000000000..c1ade7ac6c --- /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 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 0000000000..d9cd0606e7 --- /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 0000000000..04694aa5e0 --- /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 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); + window.examiner.remove(); + SimpleTest.finish(); +} + +window.addEventListener("message", receiveMessage); +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"); +} + +examiner.prototype = { + observe(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() { + 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 0000000000..09d08157da --- /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 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 be ignored within default-src when a hash or nonce is specified", + file: "file_ignore_unsafe_inline.html", + result: "acd", + }, +]; + +var counter = 0; +var curTest; + +function loadNextTest() { + if (counter == tests.length) { + document.getElementById("testframe").removeEventListener("load", test); + 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); + document.getElementById("testframe").src = src; +} + +function test() { + try { + document.getElementById("testframe").removeEventListener('load', test); + 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_ignore_xfo.html b/dom/security/test/csp/test_ignore_xfo.html new file mode 100644 index 0000000000..73c0dea44f --- /dev/null +++ b/dom/security/test/csp/test_ignore_xfo.html @@ -0,0 +1,120 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1024557: Ignore x-frame-options if CSP with frame-ancestors exists</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="csp_testframe"></iframe> +<iframe style="width:100%;" id="csp_testframe_no_xfo"></iframe> +<iframe style="width:100%;" id="csp_ro_testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* + * We load two frames using: + * x-frame-options: deny + * where the first frame uses a csp and the second a csp_ro including frame-ancestors. + * We make sure that xfo is ignored for regular csp but not for csp_ro. + */ + +SimpleTest.waitForExplicitFinish(); + +var script = SpecialPowers.loadChromeScript(() => { +/* eslint-env mozilla/chrome-script */ + let ignoreCount = 0; + function listener(msg) { + if(msg.message.includes("Content Security Policy: Ignoring ‘x-frame-options’ because of ‘frame-ancestors’ directive.")) { + ignoreCount++; + if(ignoreCount == 2) { + ok(false, 'The "Content Security Policy: Ignoring ‘x-frame-options’ because of ‘frame-ancestors’ directive." warning should only appear once for the csp_testframe.'); + } + } + } + Services.console.registerListener(listener); + + addMessageListener("cleanup", () => { + Services.console.unregisterListener(listener); + }); +}); + +SimpleTest.registerCleanupFunction(async () => { + await script.sendQuery("cleanup"); +}); + +var testcounter = 0; +function checkFinished() { + testcounter++; + if (testcounter < 4) { + return; + } + // remove the listener and we are done. + window.examiner.remove(); + SimpleTest.finish(); +} + +// X-Frame-Options checks happen in the parent, hence we have to +// proxy the xfo violation notifications. +SpecialPowers.registerObservers("xfo-on-violate-policy"); + +function examiner() { + SpecialPowers.addObserver(this, "specialpowers-xfo-on-violate-policy"); +} +examiner.prototype = { + observe(subject, topic, data) { + var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec"); + + is(asciiSpec, "http://mochi.test:8888/tests/dom/security/test/csp/file_ro_ignore_xfo.html", "correct subject"); + ok(topic.endsWith("xfo-on-violate-policy"), "correct topic"); + is(data, "deny", "correct data"); + checkFinished(); + }, + remove() { + SpecialPowers.removeObserver(this, "specialpowers-xfo-on-violate-policy"); + } +} +window.examiner = new examiner(); + +// 1) test XFO with CSP +var csp_testframe = document.getElementById("csp_testframe"); +csp_testframe.onload = function() { + var msg = csp_testframe.contentDocument.getElementById("cspmessage"); + is(msg.innerHTML, "Ignoring XFO because of CSP", "Loading frame with with XFO and CSP"); + checkFinished(); +} +csp_testframe.onerror = function() { + ok(false, "sanity: should not fire onerror for csp_testframe"); + checkFinished(); +} +csp_testframe.src = "file_ignore_xfo.html"; + +// 2) test XFO with CSP_RO +var csp_ro_testframe = document.getElementById("csp_ro_testframe"); +// If XFO denies framing then the onload event should fire. +csp_ro_testframe.onload = function() { + ok(true, "sanity: should fire onload for csp_ro_testframe"); + checkFinished(); +} +csp_ro_testframe.onerror = function() { + ok(false, "sanity: should not fire onerror for csp_ro_testframe"); + checkFinished(); +} +csp_ro_testframe.src = "file_ro_ignore_xfo.html"; + +var csp_testframe_no_xfo = document.getElementById("csp_testframe_no_xfo"); +csp_testframe_no_xfo.onload = function() { + var msg = csp_testframe_no_xfo.contentDocument.getElementById("cspmessage"); + is(msg.innerHTML, "Do not log xfo ignore warning when no xfo is set.", "Loading frame with with no XFO and CSP"); + checkFinished(); +} +csp_testframe_no_xfo.onerror = function() { + ok(false, "sanity: should not fire onerror for csp_testframe_no_xfo"); + checkFinished(); +} +csp_testframe_no_xfo.src = "file_no_log_ignore_xfo.html"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_image_document.html b/dom/security/test/csp/test_image_document.html new file mode 100644 index 0000000000..eba83f95a7 --- /dev/null +++ b/dom/security/test/csp/test_image_document.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1627235: Test CSP for images loaded as iframe</title> + <script 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"> + +SimpleTest.waitForExplicitFinish(); + +let testframe = document.getElementById("testframe"); + +testframe.onload = function() { + ok(true, "sanity: should fire onload for image document"); + + let contentDoc = SpecialPowers.wrap(testframe.contentDocument); + let cspJSON = contentDoc.cspJSON; + ok(cspJSON.includes("default-src"), "found default-src directive"); + ok(cspJSON.includes("https://bug1627235.test.com"), "found default-src value"); + SimpleTest.finish(); +} +testframe.onerror = function() { + ok(false, "sanity: should not fire onerror for image document"); +} +testframe.src = "file_image_document_pixel.png"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_image_nonce.html b/dom/security/test/csp/test_image_nonce.html new file mode 100644 index 0000000000..dd6bc13922 --- /dev/null +++ b/dom/security/test/csp/test_image_nonce.html @@ -0,0 +1,60 @@ +<!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 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 three images: (a) with a matching nonce, + (b) with a non matching nonce, + * (c) with no nonce + * and make sure that all three images get blocked because + * "img-src nonce-bla" should not allow an image load, not + * even if the nonce matches*. + */ + +SimpleTest.waitForExplicitFinish(); + +var counter = 0; + +function finishTest() { + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +function checkResults(aResult) { + counter++; + if (aResult === "img-with-matching-nonce-blocked" || + aResult === "img-with_non-matching-nonce-blocked" || + aResult === "img-without-nonce-blocked") { + ok (true, "correct result for: " + aResult); + } + else { + ok(false, "unexpected result: " + aResult + "\n\n"); + } + if (counter < 3) { + return; + } + finishTest(); +} + +// 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); +function receiveMessage(event) { + checkResults(event.data.result); +} + +document.getElementById("testframe").src = "file_image_nonce.html"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_independent_iframe_csp.html b/dom/security/test/csp/test_independent_iframe_csp.html new file mode 100644 index 0000000000..9549263a11 --- /dev/null +++ b/dom/security/test/csp/test_independent_iframe_csp.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1419222 - iFrame CSP should not affect parent document CSP</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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_independent_iframe_csp.html"></iframe> + +<script class="testbody" type="text/javascript"> + + /* Description of the test: + This test makes sure adding a CSP directive to an iFrame does not propagate + the new directive to the parent document. + + The test page records it's own CSP before and after adding an iFrame + with an additional CSP directive. + + CSPs before and after adding the iFrame are compared to make sure they do + not differ. + */ + + SimpleTest.waitForExplicitFinish(); + + function finishTest() { + window.removeEventListener("message", compareCSPs); + SimpleTest.finish(); + } + + function compareCSPs(event) { + try { + var beginCspObj = event.data.result[0]; + var iFrameCspObj = event.data.result[1]; + var endCspObj = event.data.result[2]; + + // make sure the parent document had one policy from the start. + var beginPolicies = beginCspObj["csp-policies"]; + is(beginPolicies.length, 1, "The parent doc should start with one policy applied."); + + // make sure the parent document still has one policy after adding the iFrame. + var endPolicies = endCspObj["csp-policies"]; + is(endPolicies.length, 1, "The parent doc should still have one policy applied after adding the iFrame."); + + // make sure the iFrame has an additional CSP policy. + var iFramePolicies = iFrameCspObj["csp-policies"]; + is(iFramePolicies.length, 2, "The iFrame should have two policies applied"); + + var beginDirs = []; + var endDirs = []; + for (var dir in beginPolicies[0]) { + beginDirs.push(dir); + } + for (var dir in endPolicies[0]) { + endDirs.push(dir); + } + // Check correct number of CSP diretives. + is(beginDirs.length, 3, "The parent's CSP policy should contain 3 directives."); + // Compare the parent'S CSP directives before and after adding the iFrame. + ok((beginDirs.length == endDirs.length && beginDirs.every((value, index) => value == endDirs[index])), + "Begin and end CSP directives of the parent should not differ"); + } + catch (e) { + ok(false, "uuh, something went wrong within independent iFrame csp test"); + } + + finishTest(); + } + + // a postMessage handler to initiate the checks after the iFrame was added to + // the test page. + window.addEventListener("message", compareCSPs); + +</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 0000000000..99b055f0c7 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <script 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"); +} +examiner.prototype = { + observe(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() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + } +} + +function finishTest() { + window.examiner.remove(); + window.removeEventListener("message", receiveMessage); + 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); +function receiveMessage(event) { + checkResults(event.data); +} + +function clickit() { + document.getElementById("testframe").removeEventListener('load', clickit); + 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); +} + +// 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 0000000000..dc15dc5078 --- /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 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')).color; + ok('rgb(0, 255, 0)' === color, 'External Stylesheet (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('inlinestylediv')).color; + ok('rgb(0, 0, 0)' === color, 'Inline Style TAG (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('attrstylediv')).color; + ok('rgb(0, 0, 0)' === color, 'Style Attribute (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('csstextstylediv')).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')).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')).color; + ok('rgb(0, 255, 0)' === color, 'External Stylesheet (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('inlinestylediv')).color; + ok('rgb(0, 255, 0)' === color, 'Inline Style TAG (' + color + ')'); + color = window.getComputedStyle(cspframe.contentDocument.getElementById('attrstylediv')).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')).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')).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); +document.getElementById('cspframe2').src = 'file_inlinestyle_main_allowed.html'; +document.getElementById('cspframe2').addEventListener('load', checkStylesAllowed); + +</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 0000000000..c170dc2a27 --- /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 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); + document.getElementById("testframe").src = src; +} + +function test() { + try { + document.getElementById("testframe").removeEventListener('load', test); + 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 0000000000..53994b0013 --- /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 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"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(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() { + 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_link_rel_preload.html b/dom/security/test/csp/test_link_rel_preload.html new file mode 100644 index 0000000000..18f236d56a --- /dev/null +++ b/dom/security/test/csp/test_link_rel_preload.html @@ -0,0 +1,80 @@ +<!doctype html> +<html> +<head> + <title>Bug 1599791 - Test link rel=preload</title> + <script 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"> + +// Please note that 'fakeServer' does not exist because the test relies +// on "csp-on-violate-policy" , and "specialpowers-http-notify-request" +// which fire if either the request is blocked or fires. The test does +// not rely on the result of the load. + +let TOTAL_TESTS = 3; // script, style, image +let seenTests = 0; + +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(subject, topic, data) { + if (topic === "csp-on-violate-policy") { + let asciiSpec = SpecialPowers.getPrivilegedProps( + SpecialPowers.do_QueryInterface(subject, "nsIURI"), + "asciiSpec"); + if (asciiSpec.includes("fakeServer?script") || + asciiSpec.includes("fakeServer?style") || + asciiSpec.includes("fakeServer?fetch") || + asciiSpec.includes("fakeServer?font") || + asciiSpec.includes("fakeServer?image")) { + let type = asciiSpec.substring(asciiSpec.indexOf("?") + 1); + ok (true, type + " should be blocked by CSP"); + checkFinished(); + } + } + + if (topic === "specialpowers-http-notify-request") { + if (data.includes("fakeServer?script") || + data.includes("fakeServer?style") || + data.includes("fakeServer?fetch") || + data.includes("fakeServer?font") || + data.includes("fakeServer?image")) { + let type = data.substring(data.indexOf("?") + 1); + ok (false, type + " should not be loaded"); + checkFinished(); + } + } + }, + remove() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} + +window.examiner = new examiner(); + +function checkFinished() { + seenTests++; + if (seenTests == TOTAL_TESTS) { + window.examiner.remove(); + SimpleTest.finish(); + return; + } +} + +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.pushPrefEnv( + {'set':[["network.preload", true]]}, + function() { + document.getElementById("testframe").src = "file_link_rel_preload.html"; + }); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_meta_csp_self.html b/dom/security/test/csp/test_meta_csp_self.html new file mode 100644 index 0000000000..8d7d5812a9 --- /dev/null +++ b/dom/security/test/csp/test_meta_csp_self.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1387871 - CSP: Test 'self' within meta csp in data: URI iframe</title> + <script 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 data: URI into an iframe which provides a meta-csp + * including the keyword 'self'. We make sure 'self' does not + * allow a data: image to load. + */ + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + is(event.data.result, "dataFrameReady", "sanity: received msg from loaded frame"); + + var frame = document.getElementById("testframe"); + + // make sure the img was blocked + var img = SpecialPowers.wrap(frame).contentDocument.getElementById("testimg"); + is(img.naturalWidth, 0, "img should be blocked - width should be 0"); + is(img.naturalHeight, 0, "img should be blocked - height should be 0"); + + // sanity check, make sure 'self' translates into data + var contentDoc = SpecialPowers.wrap(frame).contentDocument; + // parse the cspJSON in a csp-object + var cspOBJ = JSON.parse(contentDoc.cspJSON); + ok(cspOBJ, "sanity: was able to parse the CSP JSON"); + + // make sure we only got one policy + var policies = cspOBJ["csp-policies"]; + is(policies.length, 1, "sanity: received one CSP policy"); + + var policy = policies[0]; + var val = policy['img-src']; + is(val.toString(), "'self'", "'self' should translate into data"); + SimpleTest.finish(); +} + +let DATA_URI = `data:text/html, + <html> + <head> + <meta http-equiv="Content-Security-Policy" content="img-src 'self'"> + </head> + <body onload="parent.postMessage({result:'dataFrameReady'},'*');"> + data: URI frame with meta-csp including 'self'<br/> + <img id="testimg" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" /> + </body> + </html>`; +document.getElementById("testframe").src = DATA_URI; + +</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 0000000000..42cddbacbf --- /dev/null +++ b/dom/security/test/csp/test_meta_element.html @@ -0,0 +1,91 @@ +<!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 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); + 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 contentDoc = SpecialPowers.wrap(frame.contentDocument); + var cspJSON = contentDoc.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); +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 0000000000..679512d068 --- /dev/null +++ b/dom/security/test/csp/test_meta_header_dual.html @@ -0,0 +1,135 @@ +<!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 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); + 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 contentDoc = SpecialPowers.wrap(frame.contentDocument); + var cspOBJ = JSON.parse(contentDoc.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); +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 0000000000..2f622c3a33 --- /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 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); + 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 contentDoc = SpecialPowers.wrap(frame.contentDocument); + var cspJSON = contentDoc.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); +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 0000000000..cbb981405b --- /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 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"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(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() { + 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 0000000000..2708611e6d --- /dev/null +++ b/dom/security/test/csp/test_multipartchannel.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1416045/Bug 1223743 - CSP: Check baseChannel for CSP when loading multipart channel</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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> +<iframe style="width:100%;" id="testPartCSPframe"></iframe> + +<script class="testbody" type="text/javascript"> + +var testsToRunMultipartCSP = { + rootCSP_test: false, + part1CSP_test: false, + part2CSP_test: false, +}; + +SimpleTest.waitForExplicitFinish(); + +function checkTestsCompleted() { + for (var prop in testsToRunMultipartCSP) { + // some test hasn't run yet so we're not done + if (!testsToRunMultipartCSP[prop]) { + return; + } + } + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} +/* 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. + * We also provide CSP for each part and try to load an image in each + * part and make sure the image is loaded in first part and blocked in + * second part correctly based on its CSP accordingly. + */ + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + switch (event.data.test) { + case "rootCSP_test": + is(event.data.msg, "img-blocked", "image should be blocked"); + testsToRunMultipartCSP.rootCSP_test = true; + break; + case "part1CSP_test": + is(event.data.msg, "part1-img-loaded", "Part1 image should be loaded"); + testsToRunMultipartCSP.part1CSP_test = true; + break; + case "part2CSP_test": + is(event.data.msg, "part2-img-blocked", "Part2 image should be blocked"); + testsToRunMultipartCSP.part2CSP_test = true; + break; + } + checkTestsCompleted(); +} + +// start the test +document.getElementById("testframe").src = "file_multipart_testserver.sjs?doc"; +document.getElementById("testPartCSPframe").src = + "file_multipart_testserver.sjs?partcspdoc"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_navigate_to.html b/dom/security/test/csp/test_navigate_to.html new file mode 100644 index 0000000000..357b35bb05 --- /dev/null +++ b/dom/security/test/csp/test_navigate_to.html @@ -0,0 +1,158 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1529068 Implement CSP 'navigate-to' directive</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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"> + <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 navigations are correctly + * evaluated through the "navigate-to" directive. + */ +SpecialPowers.pushPrefEnv({"set": [["security.csp.enableNavigateTo", true]]}); +SimpleTest.waitForExplicitFinish(); + +// Note: The final website for the navigation chain must always be: www.example.com +var tests = [ + { + result : "blocked", + policy : "navigate-to www.mozilla.com", + target : "http://www.example.com/" + }, + { + result : "allowed", + policy : "navigate-to www.example.com", + target : "http://www.example.com/" + }, + { + // Test path-sensitivity + result : "blocked", + policy : "navigate-to http://www.example.com/full/path/to/file", + target : "http://www.example.com/" + }, + { + // Test scheme + result : "blocked", + policy : "navigate-to https://www.example.com/", + target : "http://www.example.com/" + }, + { + // Redirect from tracking.example.com to www.example.com + result : "blocked", + policy : "navigate-to www.example.com", + target : "http://tracking.example.com/tests/dom/security/test/csp/file_navigate_to.sjs?redir=http://www.example.com/" + }, + { + // Redirect from tracking.example.com to www.example.com (Explicitly allowed) + result : "allowed", + policy : "navigate-to tracking.example.com www.example.com", + target : "http://tracking.example.com/tests/dom/security/test/csp/file_navigate_to.sjs?redir=http://www.example.com/" + }, + { + // Redirect from tracking.example.com to www.example.com ('unsafe-allow-redirects') + result : "allowed", + policy : "navigate-to 'unsafe-allow-redirects' www.example.com", + target : "http://tracking.example.com/tests/dom/security/test/csp/file_navigate_to.sjs?redir=http://www.example.com/" + }, + // No path-sensitivity after redirect + { + result : "allowed", + policy : "navigate-to tracking.example.com http://www.example.com/full/path/to/file", + target : "http://tracking.example.com/tests/dom/security/test/csp/file_navigate_to.sjs?redir=http://www.example.com/" + }, + // Multiple CSP directives, first block (origin) second allow + { + result : "allowed", + policy : "img-src 'none'; navigate-to www.example.com", + target : "http://www.example.com/" + }, + // Multiple CSP directives, first allow (origin) second block + { + result : "blocked", + policy : "img-src www.example.com mochi.test:8888; navigate-to www.mozilla.com", + target : "http://www.example.com/" + }, + // Multiple CSPs, first allow second block + { + result : "blocked", + policy : "navigate-to www.example.com", + policy2 : "navigate-to www.mozilla.com", + target : "http://www.example.com/" + }, + // Multiple CSPs, first block second allow + { + result : "blocked", + policy : "navigate-to www.mozilla.com", + policy2 : "navigate-to www.example.com", + target : "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 + + "(" + tests[counter].policy + ", " + tests[counter].target + ")!"); + 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"); +} +examiner.prototype = { + observe(subject, topic, data) { + if (topic === "csp-on-violate-policy" && data === "navigate-to") { + checkResult("blocked"); + } + + }, + remove() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + } +} +window.NavigationActionExaminer = new examiner(); +// We use iframe onload to check if requests are not blocked by CSP +var iframe = document.getElementById("testframe"); +iframe.onload = function() { + checkResult("allowed"); +} + +function loadNextTest() { + counter++; + if (counter == tests.length) { + window.NavigationActionExaminer.remove(); + SimpleTest.finish(); + return; + } + + var src = "file_navigate_to.sjs"; + // append the CSP that should be used to serve the file + src += "?csp=" + escape(tests[counter].policy); + if( tests[counter].policy2 ) { + src += "&csp2=" + escape(tests[counter].policy2); + } + src += "&target=" + escape(tests[counter].target); + + iframe.src = src; +} + +// start running the tests +loadNextTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_nonce_redirects.html b/dom/security/test/csp/test_nonce_redirects.html new file mode 100644 index 0000000000..9b9e5e347d --- /dev/null +++ b/dom/security/test/csp/test_nonce_redirects.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1469150:Scripts with valid nonce get blocked if URL redirects</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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 script with a matching nonce, which redirects + * and we make sure that script is allowed. + */ + +SimpleTest.waitForExplicitFinish(); + +function finishTest() { + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +function checkResults(aResult) { + + if (aResult === "script-loaded") { + ok(true, "expected result: script loaded"); + } + else { + ok(false, "unexpected result: script blocked"); + } + finishTest(); +} + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + checkResults(event.data.result); +} + +document.getElementById("testframe").src = "file_nonce_redirects.html"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_nonce_snapshot.html b/dom/security/test/csp/test_nonce_snapshot.html new file mode 100644 index 0000000000..6670d6868f --- /dev/null +++ b/dom/security/test/csp/test_nonce_snapshot.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1509738 - Snapshot nonce at load start time</title> + <script 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: + * a) the test starts loading a script using allowlisted nonce + * b) the nonce of the script gets modified + * c) the script hits a 302 server side redirect + * d) we ensure the script still loads and does not use the modified nonce + */ + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + is(event.data, "script-loaded", "script loaded even though nonce was dynamically modified"); + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + + +SimpleTest.waitForExplicitFinish(); +let src = "file_nonce_snapshot.sjs?load-frame"; +document.getElementById("testframe").src = src; + +</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 0000000000..e11452c6e1 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <script 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"); + SpecialPowers.addObserver(this, "csp-on-violate-policy"); +} + +examiner.prototype = { + observe(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() { + 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)).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); +</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 0000000000..324b644f83 --- /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 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); + +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_object_inherit.html b/dom/security/test/csp/test_object_inherit.html new file mode 100644 index 0000000000..0d563bde3f --- /dev/null +++ b/dom/security/test/csp/test_object_inherit.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1457100: Test OBJECT inherits CSP if needed</title> + <script 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(); + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + + var cspJSON = event.data.cspJSON; + ok(cspJSON.includes("img-src"), "found img-src directive"); + ok(cspJSON.includes("https://bug1457100.test.com"), "found img-src value"); + + SimpleTest.finish(); +} + +document.getElementById("testframe").src = "file_object_inherit.html"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_parent_location_js.html b/dom/security/test/csp/test_parent_location_js.html new file mode 100644 index 0000000000..d456c809f2 --- /dev/null +++ b/dom/security/test/csp/test_parent_location_js.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1550414: Add CSP test for setting parent location to javascript:</title> + <script 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 a document with a CSP of essentially script-src 'none' which includes a + * same origin iframe which tries to modify the parent.location using a javascript: + * URI -> make sure the javascript: URI is blocked correctly! + */ + +SimpleTest.waitForExplicitFinish(); + +function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + is(event.data.blockedURI, "inline", "blockedURI"); + is(event.data.violatedDirective, "script-src-elem", "violatedDirective") + is(event.data.originalPolicy, "script-src 'nonce-bug1550414'", "originalPolicy"); + SimpleTest.finish(); +} + +// using a postMessage handler to report the result back from +// within the sandboxed iframe without 'allow-same-origin'. +window.addEventListener("message", receiveMessage); + +document.getElementById("testframe").src = "file_parent_location_js.html"; + +</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 0000000000..a54de0a25c --- /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 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); + document.getElementById("testframe").src = src; + } +} + +function test() { + try { + document.getElementById("testframe").removeEventListener('load', test); + 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 0000000000..d3b2771d0a --- /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 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 allowlisted 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); + 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); + 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 0000000000..3f911b7b6a --- /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 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"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(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() { + 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 0000000000..8838f2fc45 --- /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 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_punycode_host_src.html b/dom/security/test/csp/test_punycode_host_src.html new file mode 100644 index 0000000000..3735275d34 --- /dev/null +++ b/dom/security/test/csp/test_punycode_host_src.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1224225 - CSP source matching should work for punycoded domain names</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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 the + * CSP matching is same for punycode domain names as well as IDNA. + */ + +SimpleTest.waitForExplicitFinish(); + + +var curTest; +var counter = -1; + +const tests = [ + { // test 1 + description: "loads script as sub2.ält.example.org, but allowlist in CSP as sub2.xn--lt-uia.example.org", + action: "script-unicode-csp-punycode", + csp: "script-src http://sub2.xn--lt-uia.example.org;", + expected: "script-allowed", + + }, + { // test 2 + description: "loads script as sub2.xn--lt-uia.example.org, and allowlist in CSP as sub2.xn--lt-uia.example.org", + action: "script-punycode-csp-punycode", + csp: "script-src http://sub2.xn--lt-uia.example.org;", + expected: "script-allowed", + + }, + { // test 3 + description: "loads script as sub2.xn--lt-uia.example.org, and allowlist in CSP as sub2.xn--lt-uia.example.org", + action: "script-punycode-csp-punycode", + csp: "script-src *.xn--lt-uia.example.org;", + expected: "script-allowed", + + }, + +]; + +function finishTest() { + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +function checkResults(result) { + is(result, curTest.expected, curTest.description); + loadNextTest(); +} + +window.addEventListener("message", receiveMessage); +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_punycode_host_src.sjs?action=${curTest.action}&csp=${curTest.csp}`; +} + +loadNextTest(); + +</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 0000000000..9cc569ed72 --- /dev/null +++ b/dom/security/test/csp/test_redirects.html @@ -0,0 +1,143 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Tests for Content Security Policy during redirects</title> + <script 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"; +} + +SpecialPowers.registerObservers("csp-on-violate-policy"); + +// used to watch if requests are blocked by CSP or allowed through +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy"); + SpecialPowers.addObserver(this, "specialpowers-csp-on-violate-policy"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(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" || topic === "specialpowers-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() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-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':[// On a cellular connection the default preload value is 0 ("preload + // none"). Our Android emulators emulate a cellular connection, and + // so by default preload no media data. This causes the media_* tests + // to timeout. We set the default used by cellular connections to the + // same as used by non-cellular connections in order to get + // consistent behavior across platforms/devices. + ["media.preload.default", 2], + ["media.preload.default.cellular", 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_report.html b/dom/security/test/csp/test_report.html new file mode 100644 index 0000000000..fc10cd0341 --- /dev/null +++ b/dom/security/test/csp/test_report.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=548193 +--> +<head> + <title>Test for Bug 548193</title> + <script 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-sample'; 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%20%27report-sample%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"], "inline", "Incorrect blocked-uri"); + + is(cspReport["effective-directive"], "script-src-elem", "Incorrect effective-directive"); + is(cspReport["violated-directive"], "script-src-elem", "Incorrect violated-directive"); + + is(cspReport["original-policy"], "default-src 'none' 'report-sample'; report-uri http://mochi.test:8888/foo.sjs", + "Incorrect original-policy"); + + is(cspReport.disposition, "enforce", "Incorrect disposition"); + + is(cspReport["status-code"], 200, "Incorrect status-code"); + + 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); + script.sendAsyncMessage("finish"); + 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_font_cache.html b/dom/security/test/csp/test_report_font_cache.html new file mode 100644 index 0000000000..40577a1e00 --- /dev/null +++ b/dom/security/test/csp/test_report_font_cache.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +<iframe id="f"></iframe> + +<script> +var chromeScriptUrl = SimpleTest.getTestFileURL("file_report_chromescript.js"); +var script = SpecialPowers.loadChromeScript(chromeScriptUrl); + +var reportedFont1 = false; +var reportedFont3 = false; + +function reportListener(msg) { + if (!msg.error) { + // Step 3: Check the specific blocked URLs from the CSP reports. + let blocked = JSON.parse(msg.report)["csp-report"]["blocked-uri"] + .replace(/^.*\//, ""); + switch (blocked) { + case "Ahem.ttf?report_font_cache-1": + ok(!reportedFont1, "should not have already reported Test Font 1"); + ok(!reportedFont3, "should not have reported Test Font 3 before Test Font 1"); + reportedFont1 = true; + break; + case "Ahem.ttf?report_font_cache-2": + ok(false, "should not have reported Test Font 2"); + break; + case "Ahem.ttf?report_font_cache-3": + ok(!reportedFont3, "should not have already reported Test Font 3"); + reportedFont3 = true; + break; + } + if (reportedFont1 && reportedFont3) { + script.removeMessageListener("opening-request-completed", reportListener); + script.sendAsyncMessage("finish"); + SimpleTest.finish(); + } + } +} + +SimpleTest.waitForExplicitFinish(); + +script.addMessageListener("opening-request-completed", reportListener); + +window.onmessage = function(message) { + // Step 2: Navigate to the second document, which will attempt to use the + // cached "Test Font 1" and then a new "Test Font 3", both of which will + // generate CSP reports. The "Test Font 2" entry in the user font cache + // should not cause a CSP report from this document. + is(message.data, "first-doc-ready"); + f.src = "file_report_font_cache-2.html"; +}; + +// Step 1: Prime the user font cache with entries for "Test Font 1", +// "Test Font 2" and "Test Font 3". +f.src = "file_report_font_cache-1.html"; +</script> 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 0000000000..ddeee3b507 --- /dev/null +++ b/dom/security/test/csp/test_report_for_import.html @@ -0,0 +1,109 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=548193 +--> +<head> + <title>Test for Bug 548193</title> + <script 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-elem", + "Incorrect violated-directive"); + is(cspReport["original-policy"], POLICY, "Incorrect original-policy"); + is(cspReport["blocked-uri"], + "http://example.com/tests/dom/security/test/csp/file_report_for_import_server.sjs?stylesheet", + "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 0000000000..0bdfd57bc9 --- /dev/null +++ b/dom/security/test/csp/test_report_uri_missing_in_report_only_header.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=847081 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 847081</title> + <script 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]); + +function cleanup() { + SpecialPowers.postConsoleSentinel(); + SimpleTest.finish(); +} + +// Since Bug 1584993 we parse the CSP in the parent too, hence the +// same error message appears twice in the console. +var recordConsoleMsgOnce = false; + +SpecialPowers.registerConsoleListener(function ConsoleMsgListener(aMsg) { + if (aMsg.message.indexOf(warningMsg) > -1) { + if (recordConsoleMsgOnce) { + return; + } + recordConsoleMsgOnce = true; + + 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_sandbox.html b/dom/security/test/csp/test_sandbox.html new file mode 100644 index 0000000000..9fa123eadf --- /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 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); + +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"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} + +examiner.prototype = { + observe(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() { + 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 0000000000..68544a5178 --- /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 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 0000000000..3de3d98d69 --- /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 allowlisting of scheme-relative sources</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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); + 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); + +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_script_template.html b/dom/security/test/csp/test_script_template.html new file mode 100644 index 0000000000..a71ebfe960 --- /dev/null +++ b/dom/security/test/csp/test_script_template.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1548385 - CSP: Test script template</title> + <script 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 document using a CSP of "default-src 'unsafe-inline'" + * and make sure that an external script within a template gets + * blocked correctly. + */ + +const CSP_BLOCKED_SUBJECT = "csp-on-violate-policy"; +const CSP_ALLOWED_SUBJECT = "specialpowers-http-notify-request"; + +SimpleTest.waitForExplicitFinish(); + +function examiner() { + SpecialPowers.addObserver(this, CSP_BLOCKED_SUBJECT); + SpecialPowers.addObserver(this, CSP_ALLOWED_SUBJECT); +} + +examiner.prototype = { + observe(subject, topic, data) { + if (topic == CSP_BLOCKED_SUBJECT) { + let jsFileName = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIURI"), "asciiSpec"); + if (jsFileName.endsWith("file_script_template.js")) { + ok(true, "js file blocked by CSP"); + this.removeAndFinish(); + } + } + + if (topic == CSP_ALLOWED_SUBJECT) { + if (data.endsWith("file_script_template.js")) { + ok(false, "js file allowed by CSP"); + this.removeAndFinish(); + } + } + }, + + removeAndFinish() { + SpecialPowers.removeObserver(this, CSP_BLOCKED_SUBJECT); + SpecialPowers.removeObserver(this, CSP_ALLOWED_SUBJECT); + SimpleTest.finish(); + } +} + +window.examiner = new examiner(); +document.getElementById("testframe").src = "file_script_template.html"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_security_policy_violation_event.html b/dom/security/test/csp/test_security_policy_violation_event.html new file mode 100644 index 0000000000..0d5cfade9c --- /dev/null +++ b/dom/security/test/csp/test_security_policy_violation_event.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<meta http-equiv="Content-Security-Policy" content="img-src 'none'"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script> +SimpleTest.waitForExplicitFinish(); + +document.addEventListener("securitypolicyviolation", (e) => { + SimpleTest.is(e.blockedURI, "http://mochi.test:8888/foo/bar.jpg", "blockedURI"); + SimpleTest.is(e.violatedDirective, "img-src", "violatedDirective") + SimpleTest.is(e.originalPolicy, "img-src 'none'", "originalPolicy"); + SimpleTest.finish(); +}); +</script> +<img src="http://mochi.test:8888/foo/bar.jpg"> 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 0000000000..50627711ff --- /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 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"]); + +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 0000000000..3b0df34c05 --- /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 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); +function receiveMessage(event) { + var result = event.data.result; + is(result, "blocked-beacon-does-not-throw", "sendBeacon should not throw"); + window.removeEventListener("message", receiveMessage); + 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 0000000000..dc3b3b43d2 --- /dev/null +++ b/dom/security/test/csp/test_service_worker.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1208559 - ServiceWorker registration not governed by CSP</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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); +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], + ["privacy.partition.serviceWorkers", 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_strict_dynamic.html b/dom/security/test/csp/test_strict_dynamic.html new file mode 100644 index 0000000000..f894e6d447 --- /dev/null +++ b/dom/security/test/csp/test_strict_dynamic.html @@ -0,0 +1,133 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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 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, allowlist 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 + desc: "strict-dynamic with DOM events should be blocked (marquee)", + result: "blocked", + file: "file_strict_dynamic_script_events_marquee.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); + document.getElementById("testframe").src = src; +} + +function test() { + try { + document.getElementById("testframe").removeEventListener('load', test); + 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 0000000000..53eb899ab2 --- /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 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 scripts and images with a CSP of 'strict-dynamic' making sure + * allowlists 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: "blocked", + 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: "blocked", + 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); + document.getElementById("testframe").src = src; +} + +function checkResults() { + try { + var testframe = document.getElementById("testframe"); + testframe.removeEventListener('load', checkResults); + + // check if script loaded + var divcontent = testframe.contentWindow.document.getElementById('testdiv').innerHTML; + var imgcontent = testframe.contentWindow.document.getElementById('testimage').dataset.result; + 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") { + todo_is(imgcontent, curTest.img_result, curTest.img_desc); + } + else { + is(imgcontent, curTest.img_result, 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 0000000000..63d2c5a256 --- /dev/null +++ b/dom/security/test/csp/test_strict_dynamic_parser_inserted.html @@ -0,0 +1,94 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1299483 - CSP: Implement 'strict-dynamic'</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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 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); + document.getElementById("testframe").src = src; +} + +function test() { + try { + document.getElementById("testframe").removeEventListener('load', test); + 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 0000000000..fbf5a885cd --- /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 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_svg_inline_style.html b/dom/security/test/csp/test_svg_inline_style.html new file mode 100644 index 0000000000..70adae7275 --- /dev/null +++ b/dom/security/test/csp/test_svg_inline_style.html @@ -0,0 +1,102 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1262842: Test CSP inline style within svg image</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe id="img_base"></iframe> +<iframe id="img_csp"></iframe> +<iframe id="doc_base"></iframe> +<iframe id="doc_csp"></iframe> + +<script class="testbody" type="text/javascript"> + +// Description of the two tests: +// * CSP should not apply to SVGs loaded as images +// * CSP should apply to SVGs loaded as document +// Since we have to test inline styles within SVGs, we loaded the SVGs +// and then take screenshots to comopare that the two SVGs are identical. + +SimpleTest.waitForExplicitFinish(); + +let img_base = document.getElementById("img_base"); +let img_csp = document.getElementById("img_csp"); +let doc_base = document.getElementById("doc_base"); +let doc_csp = document.getElementById("doc_csp"); + +let loadedFrames = 0; + +async function compareSVGs() { + loadedFrames++; + if (loadedFrames != 4) { + return; + } + // compare the two iframes where SVGs are loaded as images + try { + let img_base_snap = await snapshotWindow(img_base.contentWindow); + let img_csp_snap = await snapshotWindow(img_csp.contentWindow); + + ok(compareSnapshots(img_base_snap, img_csp_snap, true)[0], + "CSP should not apply to SVG loaded as image"); + } catch(err) { + ok(false, "img error: " + err.message); + } + + // compare the two iframes where SVGs are loaded as documents + try { + let doc_base_snap = await snapshotWindow(doc_base.contentWindow); + let doc_csp_snap = await snapshotWindow(doc_csp.contentWindow); + + ok(compareSnapshots(doc_base_snap, doc_csp_snap, true)[0], + "CSP should apply to SVG loaded as document"); + } catch(err) { + ok(false, "doc error: " + err.message); + } + + SimpleTest.finish(); +} + +// load SVG as images +img_base.onerror = function() { + ok(false, "sanity: img_base onerror should not fire"); +} +img_base.onload = function() { + ok(true, "sanity: img_base onload should fire"); + compareSVGs(); +} +img_base.src = "file_svg_inline_style_base.html"; + +img_csp.onerror = function() { + ok(false, "sanity: img_csp onerror should not fire"); +} +img_csp.onload = function() { + ok(true, "sanity: img_csp onload should fire"); + compareSVGs(); +} +img_csp.src = "file_svg_inline_style_csp.html"; + +// load SVG as documnents +doc_base.onerror = function() { + ok(false, "sanity: doc_base onerror should not fire"); +} +doc_base.onload = function() { + ok(true, "sanity: doc_base onload should fire"); + compareSVGs(); +} +doc_base.src = "file_svg_inline_style_server.sjs?svg_no_inline_style"; + +doc_csp.onerror = function() { + ok(false, "sanity: doc_csp onerror should not fire"); +} +doc_csp.onload = function() { + ok(true, "sanity: doc_csp onload should fire"); + compareSVGs(); +} +doc_csp.src = "file_svg_inline_style_server.sjs?svg_inline_style_csp"; + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_uir_top_nav.html b/dom/security/test/csp/test_uir_top_nav.html new file mode 100644 index 0000000000..57005ba6f9 --- /dev/null +++ b/dom/security/test/csp/test_uir_top_nav.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1391011: Test uir for toplevel navigations</title> + <script 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 an https page which defines upgrade-insecure-requests into an iframe + * and perform a same origin and a cross origin toplevel load and make sure that + * upgrade-insecure-requests applies to the same origin load. + */ + +let totalTests = 2; +let testCounter = 0; + +function checkResults(aResult) { + ok(aResult == "https://example.com/tests/dom/security/test/csp/file_uir_top_nav_dummy.html" || + aResult == "http://test1.example.com/tests/dom/security/test/csp/file_uir_top_nav_dummy.html", + "same origin should be upgraded to https, cross origin should remain http"); + if (++testCounter < totalTests) { + return; + } + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + checkResults(event.data.result); +} + +function startTest() { + document.getElementById("testframe").src = + "https://example.com/tests/dom/security/test/csp/file_uir_top_nav.html"; +} + +// Don't upgrade to https to test that upgrade-insecure-requests acts correctly and +// start test +SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_first", false] + ]}, startTest); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_uir_windowwatcher.html b/dom/security/test/csp/test_uir_windowwatcher.html new file mode 100644 index 0000000000..f16b3c93a6 --- /dev/null +++ b/dom/security/test/csp/test_uir_windowwatcher.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1529893 - Test upgrade-insecure-requests for opening window through nsWindowWatcher</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<iframe name='frameA' width="100%" src="http://example.com/tests/dom/security/test/csp/file_windowwatcher_frameA.html"></iframe> + +<script class="testbody" type="text/javascript"> + +// The CSP of subframe C should cause the window to be opened to be upgraded from http to https. + +SimpleTest.waitForExplicitFinish(); + +let finalURI = "https://example.com/tests/dom/security/test/csp/file_windowwatcher_win_open.html"; + +window.addEventListener("message", receiveMessage); + +function receiveMessage(event) { + is(event.data.result, finalURI, "opened window correctly upgraded to https"); + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +</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 0000000000..b0dcdeefd4 --- /dev/null +++ b/dom/security/test/csp/test_upgrade_insecure.html @@ -0,0 +1,192 @@ +<!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 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 { AppConstants } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); + +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 allowlist 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", "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", "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", "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", + "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", "nested-img-ok" + ] + }, +]; + +// TODO: WebSocket tests are not supported on Android Yet. Bug 1566168. +if (AppConstants.platform !== "android") { + for (let test of tests) { + test.results.push(test.results[0] == "iframe-ok" ? "websocket-ok" : "websocket-error"); + } +} + +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); + 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) { + 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); +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 0000000000..3ed53d8108 --- /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 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); + SimpleTest.finish(); +} + +// a postMessage handler that is used to bubble up results from +// within the iframe. +window.addEventListener("message", receiveMessage); +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 0000000000..dc6039ec35 --- /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 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); +function receiveMessage(event) { + is(event.data.result, RESULT, "doc.write(iframe) of http should be upgraded to https!"); + window.removeEventListener("message", receiveMessage); + 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_loopback.html b/dom/security/test/csp/test_upgrade_insecure_loopback.html new file mode 100644 index 0000000000..f72f95215e --- /dev/null +++ b/dom/security/test/csp/test_upgrade_insecure_loopback.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1447784 - Implement CSP upgrade-insecure-requests directive</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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 performs a CORS XHR to 127.0.0.1 which shouldn't be upgraded to https: + * + * Test 1: + * Main page: https://127.0.0.1:8080 + * XHR request: http://127.0.0.1:8080 + * No redirect to https:// + * Description: Upgrade insecure should *NOT* upgrade from http to https. + */ + +const CSP_POLICY = "upgrade-insecure-requests; script-src 'unsafe-inline'"; +let testFiles = ["tests/dom/security/test/csp/file_upgrade_insecure_loopback.html", + "tests/dom/security/test/csp/file_upgrade_insecure_loopback_form.html"]; + +function examiner() { + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(subject, topic, data) { + if (topic === "specialpowers-http-notify-request") { + // we skip looking at other requests that might be observed accidentally + // e.g., we saw kinto requests when running this test locally + if (data.includes("bug-1661423-dont-upgrade-localhost")) { + let urlObj = new URL(data); + is(urlObj.protocol, "http:", "Didn't upgrade localhost URL"); + loadTest(); + } + } + }, + remove() { + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +}; + +window.examiner = new examiner(); + + +function loadTest() { + if (!testFiles.length) { + removeAndFinish(); + return; + } + var src = "https://example.com/tests/dom/security/test/csp/file_testserver.sjs?file="; + // append the file that should be served + src += escape(testFiles.shift()) + // append the CSP that should be used to serve the file + src += "&csp=" + escape(CSP_POLICY); + document.getElementById("testframe").src = src; +} + +function removeAndFinish() { + window.removeEventListener("message", receiveMessage); + window.examiner.remove(); + SimpleTest.finish(); +} + +// a postMessage handler that is used to bubble up results from +// within the iframe. +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + if (event.data === "request-not-https") { + ok(true, "Didn't upgrade 127.0.0.1:8080 to https://"); + loadTest(); + } +} + +SimpleTest.waitForExplicitFinish(); + +// By default, proxies don't apply to 127.0.0.1. +// We need them to for this test (at least on android), though: +SpecialPowers.pushPrefEnv({set: [ + ["network.proxy.allow_hijacking_localhost", true] +]}).then(loadTest); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_upgrade_insecure_navigation.html b/dom/security/test/csp/test_upgrade_insecure_navigation.html new file mode 100644 index 0000000000..5694deb15a --- /dev/null +++ b/dom/security/test/csp/test_upgrade_insecure_navigation.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1271173 - Missing spec on Upgrade Insecure Requests(Navigational Upgrades) </title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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> +<iframe style="width:100%;" id="sandboxedtestframe" + sandbox="allow-scripts allow-top-navigation allow-same-origin allow-pointer-lock allow-popups"></iframe> + +<script class="testbody" type="text/javascript"> +/* + * Description of the test: + * We load a page into an iframe that performs a navigational request. + * We make sure that upgrade-insecure-requests applies and the page + * gets upgraded to https if same origin. + * Please note that uir only applies to sandboxed iframes if + * the value 'allow-same-origin' is specified. + */ + +SimpleTest.waitForExplicitFinish(); + +var tests = [ + { + csp: "upgrade-insecure-requests;", + result: "https", + origin: "http://example.com", + desc: "upgrade-insecure-requests same origin should upgrade" + }, + { + csp: "", + result: "http", + origin: "http://example.com", + desc: "No upgrade-insecure-requests same origin should not upgrade" + }, + { + csp: "upgrade-insecure-requests;", + result: "http", + origin: "http://mochi.test:8888", + desc: "upgrade-insecure-requests cross origin should not upgrade" + }, + { + csp: "", + result: "http", + origin: "http://mochi.test:8888", + desc: "No upgrade-insecure-requests cross origin should not upgrade" + }, +]; + +// initializing to -1 so we start at index 0 when we start the test +var counter = -1; + +function finishTest() { + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +var subtests = 0; + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + var result = event.data.result; + // query the scheme from the URL before comparing the result + var scheme = result.substring(0, result.indexOf(":")); + is(scheme, tests[counter].result, tests[counter].desc); + + // @hardcoded 4: + // each test run contains of two subtests (frame and top-level) + // and we load each test into a regular iframe and into a + // sandboxed iframe. only move on to the next test once all + // four results from the subtests have bubbled up. + subtests++; + if (subtests != 4) { + return; + } + subtests = 0; + loadNextTest(); +} + +function loadNextTest() { + counter++; + if (counter == tests.length) { + finishTest(); + return; + } + + var src = tests[counter].origin; + src += "/tests/dom/security/test/csp/file_upgrade_insecure_navigation.sjs"; + src += "?csp=" + escape(tests[counter].csp); + src += "&action=perform_navigation"; + document.getElementById("testframe").src = src; + document.getElementById("sandboxedtestframe").src = src; +} +// Don't upgrade to https to test that upgrade-insecure-requests acts correctly +// start running the tests +SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", false]] +}, loadNextTest); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_upgrade_insecure_navigation_redirect.html b/dom/security/test/csp/test_upgrade_insecure_navigation_redirect.html new file mode 100644 index 0000000000..af25577cbb --- /dev/null +++ b/dom/security/test/csp/test_upgrade_insecure_navigation_redirect.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1422284 - Upgrade insecure requests should only apply to top-level same-origin redirects </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe id="redirect_same_origin_frame"></iframe> +<iframe id="redirect_cross_origin_frame"></iframe> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +let testCounter = 0; + +function checkFinished() { + // hardcoded 2 because we have a same-origin and a cross-origin test + if (++testCounter == 2) { + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); + return; + } +} + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + let docURI = event.data.docURI; + let url = docURI.split('?')[0]; + let query = docURI.split('?')[1]; + + if (query === "finaldoc_same_origin_redirect") { + // scheme schould be https + is ( + url, + "https://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs", + "upgrade-insecure-requests same origin redirect should upgrade", + ); + } + else if (query === "finaldoc_cross_origin_redirect") { + // scheme schould be http + is ( + url, + "http://test1.example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect.sjs", + "upgrade-insecure-requests cross origin redirect should not upgrade", + ); + } + else { + ok(false, "sanity: how can we ever get here?"); + } + checkFinished(); +} + +function startTest() { + document.getElementById("redirect_same_origin_frame").src = + "http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect_same_origin.html"; + document.getElementById("redirect_cross_origin_frame").src = + "http://example.com/tests/dom/security/test/csp/file_upgrade_insecure_navigation_redirect_cross_origin.html"; +} + +// do not upgrade tests by https-first, only by UIR for this test +SpecialPowers.pushPrefEnv({ set: [["dom.security.https_first", false]]}, startTest); + +</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 0000000000..4966b8627e --- /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 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); + 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); +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/csp/test_websocket_localhost.html b/dom/security/test/csp/test_websocket_localhost.html new file mode 100644 index 0000000000..6bcc93fceb --- /dev/null +++ b/dom/security/test/csp/test_websocket_localhost.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1729897: Allow unsecure websocket from localhost page with CSP: upgrade-insecure </title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="test_ws_self_frame"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ is(event.data.result, "self-ws-loaded", "websocket loaded");
+ ok(event.data.url.startsWith("ws://"), `scheme must be ws:// but got ${event.data.url}`);
+ finishTest();
+}
+
+SpecialPowers.pushPrefEnv({set: [
+ ["network.proxy.allow_hijacking_localhost", true],
+ ["network.proxy.testing_localhost_is_secure_when_hijacked", true],
+]}).then(function() {
+ const HOST = "http://localhost/tests/dom/security/test/csp/";
+ var test_ws_self_frame = document.getElementById("test_ws_self_frame");
+ test_ws_self_frame.src = HOST + "file_websocket_csp_upgrade.html";
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_websocket_self.html b/dom/security/test/csp/test_websocket_self.html new file mode 100644 index 0000000000..3eae83bfbf --- /dev/null +++ b/dom/security/test/csp/test_websocket_self.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1345615: Allow websocket schemes when using 'self' in CSP</title>
+ <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe style="width:100%;" id="test_ws_self_frame"></iframe>
+<iframe style="width:100%;" id="test_ws_explicit_frame"></iframe>
+
+<script class="testbody" type="text/javascript">
+
+/* Description of the test:
+ * We load an iframe using connect-src 'self' and one
+ * iframe using connect-src ws: and make
+ * sure that in both cases ws: as well as wss: is allowed to load.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+function finishTest() {
+ window.removeEventListener("message", receiveMessage);
+ SimpleTest.finish();
+}
+
+const TOTAL_TESTS = 4;
+var counter = 0;
+
+function checkResults(result) {
+ counter++;
+ if (result === "self-ws-loaded" || result === "self-wss-loaded" ||
+ result === "explicit-ws-loaded" || result === "explicit-wss-loaded") {
+ ok(true, "Evaluating: " + result);
+ }
+ else {
+ ok(false, "Evaluating: " + result);
+ }
+ if (counter < TOTAL_TESTS) {
+ return;
+ }
+ finishTest();
+}
+
+window.addEventListener("message", receiveMessage);
+function receiveMessage(event) {
+ checkResults(event.data.result);
+}
+
+const HOST = "http://example.com/tests/dom/security/test/csp/";
+var test_ws_self_frame = document.getElementById("test_ws_self_frame");
+test_ws_self_frame.src = HOST + "file_websocket_self.html";
+
+var test_ws_explicit_frame = document.getElementById("test_ws_explicit_frame");
+test_ws_explicit_frame.src = HOST + "file_websocket_explicit.html";
+
+</script>
+</body>
+</html>
diff --git a/dom/security/test/csp/test_win_open_blocked.html b/dom/security/test/csp/test_win_open_blocked.html new file mode 100644 index 0000000000..1335c9d272 --- /dev/null +++ b/dom/security/test/csp/test_win_open_blocked.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<html> +<head> + <!-- we have to allowlist the actual script that spawns the tests, + hence the nonce.--> + <meta http-equiv="Content-Security-Policy" content="default-src 'none'; + script-src 'nonce-foo'; style-src 'nonce-foo'"> + <script nonce="foo" src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link nonce="foo" rel="stylesheet" type="text/css" + href="/tests/SimpleTest/test.css"/> + <!-- this script block with window.open and document.open will not + be executed, since default-src is none --> + <script> + let win = window.open('file_default_src_none_csp.html'); + document.open(); + document.write("<script type='application/javascript'>" + + " window.opener.postMessage('document-opened', '*');" + + "<\/script>"); + document.close(); + </script> + <script nonce="foo"> + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("have to test that opening a " + + "new window/document has not succeeded"); + window.addEventListener("message", receiveMessage); + let checkWindowStatus = false; + let checkDocumentStatus = false; + + function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + if (event.data == "window-opened") { + checkWindowStatus = true; + win.close(); + } + if (event.data == "document-opened") { + checkDocumentStatus = true; + doc.close(); + } + } + setTimeout(function () { + is(checkWindowStatus, false, + "window shouldn't be opened"); + is(checkDocumentStatus, false, + "document shouldn't be opened"); + SimpleTest.finish(); + }, 1500); + </script> +</head> +<body> +</body> +</html> diff --git a/dom/security/test/csp/test_worker_src.html b/dom/security/test/csp/test_worker_src.html new file mode 100644 index 0000000000..5aa8f7bc56 --- /dev/null +++ b/dom/security/test/csp/test_worker_src.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1302667 - Test worker-src</title> + <script 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(); +SimpleTest.requestLongerTimeout(3); + +/* Description of the test: + * We load a page inlcuding a worker, a shared worker as well as a + * service worker with a CSP of: + * >> worker-src https://example.com; child-src 'none'; script-src 'nonce-foo' + * and make sure that worker-src governs these three kinds of workers correctly. + * In addition, we make sure that child-src as well as script-src is discarded + * in case worker-src is specified. Ideally we would use "script-src 'none'" but + * we have to allowlist the actual script that spawns the workers, hence the nonce. + */ + +let ALLOWED_HOST = "https://example.com/tests/dom/security/test/csp/"; +let BLOCKED_HOST = "https://test1.example.com/tests/dom/security/test/csp/"; + +let TESTS = [ + // allowed + ALLOWED_HOST + "file_worker_src_worker_governs.html", + ALLOWED_HOST + "file_worker_src_child_governs.html", + ALLOWED_HOST + "file_worker_src_script_governs.html", + // blocked + BLOCKED_HOST + "file_worker_src_worker_governs.html", + BLOCKED_HOST + "file_worker_src_child_governs.html", + BLOCKED_HOST + "file_worker_src_script_governs.html", +]; + +let numberSubTests = 3; // 1 web worker, 1 shared worker, 1 service worker +let subTestCounter = 0; // keeps track of how many +let testIndex = 0; + +function checkFinish() { + subTestCounter = 0; + testIndex++; + if (testIndex < TESTS.length) { + runNextTest(); + return; + } + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + let href = event.data.href; + let result = event.data.result; + + if (href.startsWith("https://example.com")) { + if (result == "worker-allowed" || + result == "shared-worker-allowed" || + result == "service-worker-allowed") { + ok(true, "allowing worker from https://example.com (" + result + ")"); + } + else { + ok(false, "blocking worker from https://example.com (" + result + ")"); + } + } + else if (href.startsWith("https://test1.example.com")) { + if (result == "worker-blocked" || + result == "shared-worker-blocked" || + result == "service-worker-blocked") { + ok(true, "blocking worker from https://test1.example.com (" + result + ")"); + } + else { + ok(false, "allowing worker from https://test1.example.com (" + result + ")"); + } + } + else { + // sanity check, we should never enter that branch, bust just in case... + ok(false, "unexpected result: " + result); + } + subTestCounter++; + if (subTestCounter < numberSubTests) { + return; + } + checkFinish(); +} + +function runNextTest() { + document.getElementById("testframe").src = TESTS[testIndex]; +} + +SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], +]}, function() { + runNextTest(); +}); + +</script> +</body> +</html> diff --git a/dom/security/test/csp/test_xslt_inherits_csp.html b/dom/security/test/csp/test_xslt_inherits_csp.html new file mode 100644 index 0000000000..90e8372db1 --- /dev/null +++ b/dom/security/test/csp/test_xslt_inherits_csp.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1597645: Make sure XSLT inherits the CSP r=ckerschb</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<body> + <iframe src="file_xslt_inherits_csp.xml"></iframe> + +<script class="testbody"> + SimpleTest.requestCompleteLog(); + SimpleTest.waitForExplicitFinish(); + + let frame = document.querySelector("iframe"); + + window.addEventListener("load",()=>{ + let link = frame.contentWindow.document.querySelector("a"); + link.click(); // + + requestAnimationFrame(()=>{ + // Wait one Frame to let the browser catch up + // before checking the dom. + let res = !frame.contentWindow.document.body.innerText.includes("JS DID EXCECUTE"); + ok(res, "The CSP did block injected JS "); + SimpleTest.finish(); + }); + }) +</script> +</html> diff --git a/dom/security/test/csp/worker.sjs b/dom/security/test/csp/worker.sjs new file mode 100644 index 0000000000..e85df3382a --- /dev/null +++ b/dom/security/test/csp/worker.sjs @@ -0,0 +1,114 @@ +Components.utils.importGlobalProperties(["URLSearchParams"]); + +const SJS = "http://mochi.test:8888/tests/dom/security/test/csp/worker.sjs"; + +function createFetchWorker(url) { + return `fetch("${url}");`; +} + +function createXHRWorker(url) { + return ` + try { + var xhr = new XMLHttpRequest(); + xhr.open("GET", "${url}"); + xhr.send(); + } catch(ex) {} + `; +} + +function createImportScriptsWorker(url) { + return ` + try { + importScripts("${url}"); + } catch(ex) {} + `; +} + +function createChildWorkerURL(params) { + let url = SJS + "?" + params.toString(); + return `new Worker("${url}");`; +} + +function createChildWorkerBlob(params) { + let url = SJS + "?" + params.toString(); + return ` + try { + var xhr = new XMLHttpRequest(); + xhr.open("GET", "${url}"); + xhr.responseType = "blob"; + xhr.send(); + xhr.onload = () => { + new Worker(URL.createObjectURL(xhr.response));}; + } catch(ex) {} + `; +} + +function handleRequest(request, response) { + let params = new URLSearchParams(request.queryString); + + let id = params.get("id"); + let base = unescape(params.get("base")); + let child = params.has("child") ? params.get("child") : ""; + + //avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "application/javascript"); + + // Deliver the CSP policy encoded in the URL + if (params.has("csp")) { + response.setHeader( + "Content-Security-Policy", + unescape(params.get("csp")), + false + ); + } + + if (child) { + let childCsp = params.has("childCsp") ? params.get("childCsp") : ""; + params.delete("csp"); + params.delete("child"); + params.delete("childCsp"); + params.append("csp", childCsp); + + switch (child) { + case "blob": + response.write(createChildWorkerBlob(params)); + break; + + case "url": + response.write(createChildWorkerURL(params)); + break; + + default: + response.setStatusLine(request.httpVersion, 400, "Bad request"); + break; + } + + return; + } + + if (params.has("action")) { + switch (params.get("action")) { + case "fetch": + response.write(createFetchWorker(base + "?id=" + id)); + break; + + case "xhr": + response.write(createXHRWorker(base + "?id=" + id)); + break; + + case "importScripts": + response.write(createImportScriptsWorker(base + "?id=" + id)); + break; + + default: + response.setStatusLine(request.httpVersion, 400, "Bad request"); + break; + } + + return; + } + + response.write("I don't know action "); + return; +} diff --git a/dom/security/test/csp/worker_helper.js b/dom/security/test/csp/worker_helper.js new file mode 100644 index 0000000000..6c329fd5f1 --- /dev/null +++ b/dom/security/test/csp/worker_helper.js @@ -0,0 +1,91 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var _tests = []; +function addTest(test) { + _tests.push(test); +} + +function addAsyncTest(fn) { + _tests.push(() => fn().catch(ok.bind(null, false))); +} + +function runNextTest() { + if (!_tests.length) { + SimpleTest.finish(); + return; + } + const fn = _tests.shift(); + try { + fn(); + } catch (ex) { + info( + "Test function " + + (fn.name ? "'" + fn.name + "' " : "") + + "threw an exception: " + + ex + ); + } +} + +/** + * Helper to perform an XHR then blob response to create worker + */ +function doXHRGetBlob(uri) { + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", uri); + xhr.responseType = "blob"; + xhr.addEventListener("load", function() { + is( + xhr.status, + 200, + "doXHRGetBlob load uri='" + uri + "' status=" + xhr.status + ); + resolve(xhr.response); + }); + xhr.send(); + }); +} + +function removeObserver(observer) { + SpecialPowers.removeObserver(observer, "specialpowers-http-notify-request"); + SpecialPowers.removeObserver(observer, "csp-on-violate-policy"); +} + +/** + * Helper to perform an assert to check if the request should be blocked or + * allowed by CSP + */ +function assertCSPBlock(url, shouldBlock) { + return new Promise((resolve, reject) => { + let observer = { + observe(subject, topic, data) { + if (topic === "specialpowers-http-notify-request") { + if (data == url) { + is(shouldBlock, false, "Should allow request uri='" + url); + removeObserver(observer); + resolve(); + } + } + + if (topic === "csp-on-violate-policy") { + let asciiSpec = SpecialPowers.getPrivilegedProps( + SpecialPowers.do_QueryInterface(subject, "nsIURI"), + "asciiSpec" + ); + if (asciiSpec == url) { + is(shouldBlock, true, "Should block request uri='" + url); + removeObserver(observer); + resolve(); + } + } + }, + }; + + SpecialPowers.addObserver(observer, "csp-on-violate-policy"); + SpecialPowers.addObserver(observer, "specialpowers-http-notify-request"); + }); +} diff --git a/dom/security/test/general/browser.ini b/dom/security/test/general/browser.ini new file mode 100644 index 0000000000..9c5f614b36 --- /dev/null +++ b/dom/security/test/general/browser.ini @@ -0,0 +1,58 @@ +[DEFAULT] +[browser_test_toplevel_data_navigations.js] +skip-if = (verify && debug && (os == 'mac')) || (debug && (os == 'mac' || os == 'linux')) # Bug 1403815 +support-files = + file_toplevel_data_navigations.sjs + file_toplevel_data_meta_redirect.html +[browser_test_data_download.js] +support-files = + file_data_download.html +[browser_test_data_text_csv.js] +support-files = + file_data_text_csv.html +[browser_test_view_image_data_navigation.js] +support-files = + file_view_image_data_navigation.html + file_view_bg_image_data_navigation.html +[browser_test_FTP_console_warning.js] +support-files = + file_FTP_console_warning.html +[browser_test_assert_systemprincipal_documents.js] +skip-if = !nightly_build +support-files = + file_assert_systemprincipal_documents.html + file_assert_systemprincipal_documents_iframe.html +[browser_test_referrer_loadInOtherProcess.js] +[browser_test_framing_error_pages.js] +support-files = + file_framing_error_pages_csp.html + file_framing_error_pages_xfo.html + file_framing_error_pages.sjs +[browser_test_xfo_embed_object.js] +support-files = + file_framing_xfo_embed.html + file_framing_xfo_object.html + file_framing_xfo_embed_object.sjs +[browser_test_report_blocking.js] +support-files = + file_framing_error_pages_xfo.html + file_framing_error_pages_csp.html + file_framing_error_pages.sjs +[browser_same_site_cookies_bug1748693.js] +support-files = + file_same_site_cookies_bug1748693.sjs +[browser_file_nonscript.js] +support-files = + file_loads_nonscript.html + file_nonscript + file_nonscript.xyz + file_nonscript.html + file_nonscript.txt + file_nonscript.json + file_script.js +[browser_restrict_privileged_about_script.js] +# This test intentionally asserts when in debug builds. Let's rely on opt builds when in CI. +skip-if = debug +support-files = + file_about_child.html + file_1767581.js diff --git a/dom/security/test/general/browser_file_nonscript.js b/dom/security/test/general/browser_file_nonscript.js new file mode 100644 index 0000000000..052acc49dc --- /dev/null +++ b/dom/security/test/general/browser_file_nonscript.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_fileurl_nonscript_load() { + await SpecialPowers.pushPrefEnv({ + set: [["security.block_fileuri_script_with_wrong_mime", true]], + }); + + let file = getChromeDir(getResolvedURI(gTestPath)); + file.append("file_loads_nonscript.html"); + let uriString = Services.io.newFileURI(file).spec; + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uriString); + registerCleanupFunction(async function() { + BrowserTestUtils.removeTab(tab); + }); + + let counter = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + Cu.exportFunction(Assert.equal.bind(Assert), content.window, { + defineAs: "equal", + }); + content.window.postMessage("run", "*"); + + await new Promise(resolve => { + content.window.addEventListener("message", event => { + if (event.data === "done") { + resolve(); + } + }); + }); + + return content.window.wrappedJSObject.counter; + }); + + is(counter, 1, "Only one script should have run"); +}); diff --git a/dom/security/test/general/browser_restrict_privileged_about_script.js b/dom/security/test/general/browser_restrict_privileged_about_script.js new file mode 100644 index 0000000000..f44ebbaed0 --- /dev/null +++ b/dom/security/test/general/browser_restrict_privileged_about_script.js @@ -0,0 +1,66 @@ +"use strict"; + +const kChildPage = getRootDirectory(gTestPath) + "file_about_child.html"; + +const kAboutPagesRegistered = BrowserTestUtils.registerAboutPage( + registerCleanupFunction, + "test-about-privileged-with-scripts", + kChildPage, + Ci.nsIAboutModule.ALLOW_SCRIPT | + Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD | + Ci.nsIAboutModule.URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS | + Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT | + Ci.nsIAboutModule.IS_SECURE_CHROME_UI +); + +add_task(async function test_principal_click() { + await kAboutPagesRegistered; + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.skip_about_page_has_csp_assert", true]], + }); + await BrowserTestUtils.withNewTab( + "about:test-about-privileged-with-scripts", + async function(browser) { + // Wait for page to fully load + info("Waiting for tab to be loaded.."); + // let's look into the fully loaded about page + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + let channel = content.docShell.currentDocumentChannel; + is( + channel.originalURI.asciiSpec, + "about:test-about-privileged-with-scripts", + "sanity check - make sure we test the principal for the correct URI" + ); + + let triggeringPrincipal = channel.loadInfo.triggeringPrincipal; + ok( + triggeringPrincipal.isSystemPrincipal, + "loading about: from privileged page must have a triggering of System" + ); + + let contentPolicyType = channel.loadInfo.externalContentPolicyType; + is( + contentPolicyType, + Ci.nsIContentPolicy.TYPE_DOCUMENT, + "sanity check - loading a top level document" + ); + + let loadingPrincipal = channel.loadInfo.loadingPrincipal; + is( + loadingPrincipal, + null, + "sanity check - load of TYPE_DOCUMENT must have a null loadingPrincipal" + ); + ok( + !content.document.nodePrincipal.isSystemPrincipal, + "sanity check - loaded about page does not have the system principal" + ); + isnot( + content.testResult, + "fail-script-was-loaded", + "The script from https://example.com shouldn't work in an about: page." + ); + }); + } + ); +}); diff --git a/dom/security/test/general/browser_same_site_cookies_bug1748693.js b/dom/security/test/general/browser_same_site_cookies_bug1748693.js new file mode 100644 index 0000000000..2f647e8c97 --- /dev/null +++ b/dom/security/test/general/browser_same_site_cookies_bug1748693.js @@ -0,0 +1,51 @@ +"use strict"; + +const HTTPS_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const HTTP_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +function checkCookies(expectedCookies = {}) { + info(JSON.stringify(expectedCookies)); + return SpecialPowers.spawn( + gBrowser.selectedBrowser, + [expectedCookies], + async function(expectedCookies) { + let cookies = content.document.getElementById("msg").innerHTML; + info(cookies); + for (const [cookie, expected] of Object.entries(expectedCookies)) { + if (expected) { + ok(cookies.includes(cookie), `${cookie} should be sent`); + } else { + ok(!cookies.includes(cookie), `${cookie} should not be sent`); + } + } + } + ); +} + +add_task(async function bug1748693() { + waitForExplicitFinish(); + + let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + BrowserTestUtils.loadURI( + gBrowser, + `${HTTPS_PATH}file_same_site_cookies_bug1748693.sjs?setcookies` + ); + await loaded; + + loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + BrowserTestUtils.loadURI( + gBrowser, + `${HTTP_PATH}file_same_site_cookies_bug1748693.sjs` + ); + await loaded; + + await checkCookies({ auth: true, auth_secure: false }); + + finish(); +}); diff --git a/dom/security/test/general/browser_test_FTP_console_warning.js b/dom/security/test/general/browser_test_FTP_console_warning.js new file mode 100644 index 0000000000..9384f5ddd9 --- /dev/null +++ b/dom/security/test/general/browser_test_FTP_console_warning.js @@ -0,0 +1,39 @@ +// Description of the test: +// Ensure that FTP subresource loads trigger a warning in the webconsole. +"use strict"; + +// These files don't actually exist, we are just looking for messages +// that indicate that loading those files would have been blocked. +var seen_files = ["a.html", "b.html", "c.html", "d.png"]; + +function on_new_message(msgObj) { + let text = msgObj.message; + if ( + text.includes("Loading FTP subresource within http(s) page not allowed") + ) { + // Remove the file in the message from the list. + seen_files = seen_files.filter(file => { + return !text.includes(file); + }); + } +} + +const kTestPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://mochi.test:8888" +); +const kTestURI = kTestPath + "file_FTP_console_warning.html"; + +add_task(async function() { + // A longer timeout is necessary for this test than the plain mochitests + // due to opening a new tab with the web console. + requestLongerTimeout(4); + + Services.console.registerListener(on_new_message); + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, kTestURI); + await BrowserTestUtils.waitForCondition(() => seen_files.length === 0); + is(seen_files.length, 0, "All FTP subresources should be blocked"); + + Services.console.unregisterListener(on_new_message); +}); diff --git a/dom/security/test/general/browser_test_assert_systemprincipal_documents.js b/dom/security/test/general/browser_test_assert_systemprincipal_documents.js new file mode 100644 index 0000000000..08abdb7705 --- /dev/null +++ b/dom/security/test/general/browser_test_assert_systemprincipal_documents.js @@ -0,0 +1,41 @@ +//"use strict" + +const kTestPath = getRootDirectory(gTestPath); +const kTestURI = kTestPath + "file_assert_systemprincipal_documents.html"; + +add_setup(async function() { + // We expect the assertion in function + // CheckSystemPrincipalLoads as defined in + // file dom/security/nsContentSecurityManager.cpp + SimpleTest.expectAssertions(1); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.disallow_non_local_systemprincipal_in_tests", true], + ["security.allow_unsafe_parent_loads", true], + ], + }); +}); + +add_task(async function open_test_iframe_in_tab() { + // This looks at the iframe (load type SUBDOCUMENT) + await BrowserTestUtils.withNewTab( + { gBrowser, url: kTestURI }, + async browser => { + await SpecialPowers.spawn(browser, [], async function() { + let outerPrincipal = content.document.nodePrincipal; + ok( + outerPrincipal.isSystemPrincipal, + "Sanity: Using SystemPrincipal for test file on chrome://" + ); + const iframeDoc = content.document.getElementById("testframe") + .contentDocument; + is( + iframeDoc.body.innerHTML, + "", + "iframe with systemprincipal should be empty document" + ); + }); + } + ); +}); diff --git a/dom/security/test/general/browser_test_data_download.js b/dom/security/test/general/browser_test_data_download.js new file mode 100644 index 0000000000..5ba7877222 --- /dev/null +++ b/dom/security/test/general/browser_test_data_download.js @@ -0,0 +1,115 @@ +"use strict"; + +const kTestPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); +const kTestURI = kTestPath + "file_data_download.html"; + +function addWindowListener(aURL) { + return new Promise(resolve => { + Services.wm.addListener({ + onOpenWindow(aXULWindow) { + info("window opened, waiting for focus"); + Services.wm.removeListener(this); + var domwindow = aXULWindow.docShell.domWindow; + waitForFocus(function() { + is( + domwindow.document.location.href, + aURL, + "should have seen the right window open" + ); + resolve(domwindow); + }, domwindow); + }, + onCloseWindow(aXULWindow) {}, + }); + }); +} + +function waitDelay(delay) { + return new Promise((resolve, reject) => { + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + window.setTimeout(resolve, delay); + }); +} + +function promisePanelOpened() { + if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") { + return Promise.resolve(); + } + return BrowserTestUtils.waitForEvent(DownloadsPanel.panel, "popupshown"); +} + +add_task(async function test_with_downloads_pref_disabled() { + waitForExplicitFinish(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.data_uri.block_toplevel_data_uri_navigations", true], + ["browser.download.always_ask_before_handling_new_types", true], + ["browser.download.improvements_to_download_panel", false], + ], + }); + let windowPromise = addWindowListener( + "chrome://mozapps/content/downloads/unknownContentType.xhtml" + ); + BrowserTestUtils.loadURI(gBrowser, kTestURI); + let win = await windowPromise; + + is( + win.document.getElementById("location").value, + "data-foo.html", + "file name of download should match" + ); + + let mainWindowActivated = BrowserTestUtils.waitForEvent(window, "activate"); + await BrowserTestUtils.closeWindow(win); + await mainWindowActivated; +}); + +add_task(async function test_with_always_ask_pref_disabled() { + waitForExplicitFinish(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.data_uri.block_toplevel_data_uri_navigations", true], + ["browser.download.always_ask_before_handling_new_types", false], + ["browser.download.improvements_to_download_panel", true], + ], + }); + let downloadsPanelPromise = promisePanelOpened(); + let downloadsPromise = Downloads.getList(Downloads.PUBLIC); + + BrowserTestUtils.loadURI(gBrowser, kTestURI); + // wait until downloadsPanel opens before continuing with test + await downloadsPanelPromise; + let downloadList = await downloadsPromise; + + is(DownloadsPanel.isPanelShowing, true, "DownloadsPanel should be open."); + is( + downloadList._downloads.length, + 1, + "File should be successfully downloaded." + ); + + let [download] = downloadList._downloads; + is(download.contentType, "text/html", "File contentType should be correct."); + is( + download.source.url, + "data:text/html,<body>data download</body>", + "File name should be correct." + ); + + info("cleaning up downloads"); + try { + if (Services.appinfo.OS === "WINNT") { + // We need to make the file writable to delete it on Windows. + await IOUtils.setPermissions(download.target.path, 0o600); + } + await IOUtils.remove(download.target.path); + } catch (error) { + info("The file " + download.target.path + " is not removed, " + error); + } + + await downloadList.remove(download); + await download.finalize(); +}); diff --git a/dom/security/test/general/browser_test_data_text_csv.js b/dom/security/test/general/browser_test_data_text_csv.js new file mode 100644 index 0000000000..ac29a63491 --- /dev/null +++ b/dom/security/test/general/browser_test_data_text_csv.js @@ -0,0 +1,110 @@ +"use strict"; + +const kTestPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); +const kTestURI = kTestPath + "file_data_text_csv.html"; + +function addWindowListener(aURL, aCallback) { + return new Promise(resolve => { + Services.wm.addListener({ + onOpenWindow(aXULWindow) { + info("window opened, waiting for focus"); + Services.wm.removeListener(this); + var domwindow = aXULWindow.docShell.domWindow; + waitForFocus(function() { + is( + domwindow.document.location.href, + aURL, + "should have seen the right window open" + ); + resolve(domwindow); + }, domwindow); + }, + onCloseWindow(aXULWindow) {}, + }); + }); +} + +function promisePanelOpened() { + if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") { + return Promise.resolve(); + } + return BrowserTestUtils.waitForEvent(DownloadsPanel.panel, "popupshown"); +} + +add_task(async function test_with_pref_enabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.data_uri.block_toplevel_data_uri_navigations", true], + ["browser.download.always_ask_before_handling_new_types", true], + ["browser.download.improvements_to_download_panel", false], + ], + }); + + let windowPromise = addWindowListener( + "chrome://mozapps/content/downloads/unknownContentType.xhtml" + ); + BrowserTestUtils.loadURI(gBrowser, kTestURI); + let win = await windowPromise; + + let expectedValue = "index.csv"; + is( + win.document.getElementById("location").value, + expectedValue, + "file name of download should match" + ); + let mainWindowActivated = BrowserTestUtils.waitForEvent(window, "activate"); + await BrowserTestUtils.closeWindow(win); + await mainWindowActivated; +}); + +add_task(async function test_with_pref_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.data_uri.block_toplevel_data_uri_navigations", true], + ["browser.download.always_ask_before_handling_new_types", false], + ["browser.download.improvements_to_download_panel", true], + ], + }); + let downloadsPanelPromise = promisePanelOpened(); + let downloadsPromise = Downloads.getList(Downloads.PUBLIC); + let sourceURLBit = "text/csv;foo,bar,foobar"; + + info("Loading URI for pref enabled"); + BrowserTestUtils.loadURI(gBrowser, kTestURI); + info("Waiting for downloads panel to open"); + await downloadsPanelPromise; + info("Getting downloads info after opening downloads panel"); + let downloadList = await downloadsPromise; + + is(DownloadsPanel.isPanelShowing, true, "DownloadsPanel should be open."); + is( + downloadList._downloads.length, + 1, + "File should be successfully downloaded." + ); + + let [download] = downloadList._downloads; + is(download.contentType, "text/csv", "File contentType should be correct."); + is( + download.source.url, + `data:${sourceURLBit}`, + "File name should be correct." + ); + + info("Cleaning up downloads"); + try { + if (Services.appinfo.OS === "WINNT") { + // We need to make the file writable to delete it on Windows. + await IOUtils.setPermissions(download.target.path, 0o600); + } + await IOUtils.remove(download.target.path); + } catch (ex) { + info("The file " + download.target.path + " is not removed, " + ex); + } + + await downloadList.remove(download); + await download.finalize(); +}); diff --git a/dom/security/test/general/browser_test_framing_error_pages.js b/dom/security/test/general/browser_test_framing_error_pages.js new file mode 100644 index 0000000000..8c49494669 --- /dev/null +++ b/dom/security/test/general/browser_test_framing_error_pages.js @@ -0,0 +1,53 @@ +"use strict"; + +const kTestPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const kTestXFrameOptionsURI = kTestPath + "file_framing_error_pages_xfo.html"; +const kTestXFrameOptionsURIFrame = + kTestPath + "file_framing_error_pages.sjs?xfo"; + +const kTestFrameAncestorsURI = kTestPath + "file_framing_error_pages_csp.html"; +const kTestFrameAncestorsURIFrame = + kTestPath + "file_framing_error_pages.sjs?csp"; + +add_task(async function open_test_xfo_error_page() { + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + let loaded = BrowserTestUtils.browserLoaded( + browser, + true, + kTestXFrameOptionsURIFrame, + true + ); + BrowserTestUtils.loadURI(browser, kTestXFrameOptionsURI); + await loaded; + + await SpecialPowers.spawn(browser, [], async function() { + const iframeDoc = content.document.getElementById("testframe") + .contentDocument; + let errorPage = iframeDoc.body.innerHTML; + ok(errorPage.includes("csp-xfo-error-title"), "xfo error page correct"); + }); + }); +}); + +add_task(async function open_test_csp_frame_ancestor_error_page() { + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + let loaded = BrowserTestUtils.browserLoaded( + browser, + true, + kTestFrameAncestorsURIFrame, + true + ); + BrowserTestUtils.loadURI(browser, kTestFrameAncestorsURI); + await loaded; + + await SpecialPowers.spawn(browser, [], async function() { + const iframeDoc = content.document.getElementById("testframe") + .contentDocument; + let errorPage = iframeDoc.body.innerHTML; + ok(errorPage.includes("csp-xfo-error-title"), "csp error page correct"); + }); + }); +}); diff --git a/dom/security/test/general/browser_test_referrer_loadInOtherProcess.js b/dom/security/test/general/browser_test_referrer_loadInOtherProcess.js new file mode 100644 index 0000000000..d25f3e0802 --- /dev/null +++ b/dom/security/test/general/browser_test_referrer_loadInOtherProcess.js @@ -0,0 +1,153 @@ +const TEST_PAGE = + "https://example.org/browser/browser/base/content/test/general/dummy_page.html"; +const TEST_REFERRER = "http://mochi.test:8888/"; + +const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +let referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.ORIGIN, + true, + Services.io.newURI(TEST_REFERRER) +); +let deReferrerInfo = E10SUtils.serializeReferrerInfo(referrerInfo); + +var checkResult = async function(isRemote, browserKey, uri) { + is( + gBrowser.selectedBrowser.isRemoteBrowser, + isRemote, + "isRemoteBrowser should be correct" + ); + + is( + gBrowser.selectedBrowser.permanentKey, + browserKey, + "browser.permanentKey should be correct" + ); + + if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { + let sessionHistory = + gBrowser.selectedBrowser.browsingContext.sessionHistory; + let entry = sessionHistory.getEntryAtIndex(sessionHistory.count - 1); + let args = { uri, referrerInfo: deReferrerInfo, isRemote }; + Assert.equal(entry.URI.spec, args.uri, "Uri should be correct"); + + // Main process like about:mozilla does not trigger the real network request. + // So we don't store referrerInfo in sessionHistory in that case. + // Besides, the referrerInfo stored in sessionHistory was computed, we only + // check pre-computed things. + if (args.isRemote) { + let resultReferrerInfo = entry.referrerInfo; + let expectedReferrerInfo = E10SUtils.deserializeReferrerInfo( + args.referrerInfo + ); + + Assert.equal( + resultReferrerInfo.originalReferrer.spec, + expectedReferrerInfo.originalReferrer.spec, + "originalReferrer should be correct" + ); + Assert.equal( + resultReferrerInfo.sendReferrer, + expectedReferrerInfo.sendReferrer, + "sendReferrer should be correct" + ); + Assert.equal( + resultReferrerInfo.referrerPolicy, + expectedReferrerInfo.referrerPolicy, + "referrerPolicy should be correct" + ); + } else { + Assert.equal(entry.referrerInfo, null, "ReferrerInfo should be correct"); + } + + return; + } + + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [{ uri, referrerInfo: deReferrerInfo, isRemote }], + async function(args) { + let webNav = content.docShell.QueryInterface(Ci.nsIWebNavigation); + let sessionHistory = webNav.sessionHistory; + let entry = sessionHistory.legacySHistory.getEntryAtIndex( + sessionHistory.count - 1 + ); + + var { E10SUtils } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/E10SUtils.jsm" + ); + + Assert.equal(entry.URI.spec, args.uri, "Uri should be correct"); + + // Main process like about:mozilla does not trigger the real network request. + // So we don't store referrerInfo in sessionHistory in that case. + // Besides, the referrerInfo stored in sessionHistory was computed, we only + // check pre-computed things. + if (args.isRemote) { + let resultReferrerInfo = entry.referrerInfo; + let expectedReferrerInfo = E10SUtils.deserializeReferrerInfo( + args.referrerInfo + ); + + Assert.equal( + resultReferrerInfo.originalReferrer.spec, + expectedReferrerInfo.originalReferrer.spec, + "originalReferrer should be correct" + ); + Assert.equal( + resultReferrerInfo.sendReferrer, + expectedReferrerInfo.sendReferrer, + "sendReferrer should be correct" + ); + Assert.equal( + resultReferrerInfo.referrerPolicy, + expectedReferrerInfo.referrerPolicy, + "referrerPolicy should be correct" + ); + } else { + Assert.equal( + entry.referrerInfo, + null, + "ReferrerInfo should be correct" + ); + } + } + ); +}; +var waitForLoad = async function(uri) { + info("waitForLoad " + uri); + let loadURIOptions = { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + referrerInfo, + }; + gBrowser.selectedBrowser.webNavigation.loadURI(uri, loadURIOptions); + + await BrowserTestUtils.browserStopped(gBrowser, uri); +}; + +// Tests referrerInfo when navigating from a page in the remote process to main +// process and vice versa. +add_task(async function test_navigation() { + // Navigate from non remote to remote + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + let testURI = TEST_PAGE; + let { permanentKey } = gBrowser.selectedBrowser; + await waitForLoad(testURI); + await checkResult(true, permanentKey, testURI); + gBrowser.removeCurrentTab(); + + // Navigate from remote to non-remote + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE); + // Wait for the non-blank page to finish loading + await BrowserTestUtils.browserStopped(gBrowser, TEST_PAGE); + testURI = "about:mozilla"; + permanentKey = gBrowser.selectedBrowser.permanentKey; + await waitForLoad(testURI); + await checkResult(false, permanentKey, testURI); + + gBrowser.removeCurrentTab(); +}); diff --git a/dom/security/test/general/browser_test_report_blocking.js b/dom/security/test/general/browser_test_report_blocking.js new file mode 100644 index 0000000000..712330216a --- /dev/null +++ b/dom/security/test/general/browser_test_report_blocking.js @@ -0,0 +1,218 @@ +"use strict"; + +const { TelemetryArchiveTesting } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryArchiveTesting.sys.mjs" +); + +const kTestPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +const kTestXFrameOptionsURI = kTestPath + "file_framing_error_pages_xfo.html"; +const kTestCspURI = kTestPath + "file_framing_error_pages_csp.html"; +const kTestXFrameOptionsURIFrame = + kTestPath + "file_framing_error_pages.sjs?xfo"; +const kTestCspURIFrame = kTestPath + "file_framing_error_pages.sjs?csp"; + +const kTestExpectedPingXFO = [ + [["payload", "error_type"], "xfo"], + [["payload", "xfo_header"], "deny"], + [["payload", "csp_header"], ""], + [["payload", "frame_hostname"], "example.com"], + [["payload", "top_hostname"], "example.com"], + [ + ["payload", "frame_uri"], + "https://example.com/browser/dom/security/test/general/file_framing_error_pages.sjs", + ], + [ + ["payload", "top_uri"], + "https://example.com/browser/dom/security/test/general/file_framing_error_pages_xfo.html", + ], +]; + +const kTestExpectedPingCSP = [ + [["payload", "error_type"], "csp"], + [["payload", "xfo_header"], ""], + [["payload", "csp_header"], "'none'"], + [["payload", "frame_hostname"], "example.com"], + [["payload", "top_hostname"], "example.com"], + [ + ["payload", "frame_uri"], + "https://example.com/browser/dom/security/test/general/file_framing_error_pages.sjs", + ], + [ + ["payload", "top_uri"], + "https://example.com/browser/dom/security/test/general/file_framing_error_pages_csp.html", + ], +]; + +const TEST_CASES = [ + { + type: "xfo", + test_uri: kTestXFrameOptionsURI, + frame_uri: kTestXFrameOptionsURIFrame, + expected_ping: kTestExpectedPingXFO, + }, + { + type: "csp", + test_uri: kTestCspURI, + frame_uri: kTestCspURIFrame, + expected_ping: kTestExpectedPingCSP, + }, +]; + +add_setup(async function() { + Services.telemetry.setEventRecordingEnabled("security.ui.xfocsperror", true); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.xfocsp.errorReporting.enabled", true], + ["security.xfocsp.errorReporting.automatic", false], + ], + }); +}); + +add_task(async function testReportingCases() { + for (const test of TEST_CASES) { + await testReporting(test); + } +}); + +async function testReporting(test) { + // Clear telemetry event before testing. + Services.telemetry.clearEvents(); + + let telemetryChecker = new TelemetryArchiveTesting.Checker(); + await telemetryChecker.promiseInit(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + let browser = tab.linkedBrowser; + + let loaded = BrowserTestUtils.browserLoaded( + browser, + true, + test.frame_uri, + true + ); + BrowserTestUtils.loadURI(browser, test.test_uri); + await loaded; + + let { type } = test; + + let frameBC = await SpecialPowers.spawn(browser, [], async _ => { + const iframe = content.document.getElementById("testframe"); + return iframe.browsingContext; + }); + + await SpecialPowers.spawn(frameBC, [type], async obj => { + // Wait until the reporting UI is visible. + await ContentTaskUtils.waitForCondition(() => { + let reportUI = content.document.getElementById("blockingErrorReporting"); + return ContentTaskUtils.is_visible(reportUI); + }); + + let reportCheckBox = content.document.getElementById( + "automaticallyReportBlockingInFuture" + ); + is( + reportCheckBox.checked, + false, + "The checkbox of the reporting ui should be not checked." + ); + + // Click on the checkbox. + await EventUtils.synthesizeMouseAtCenter(reportCheckBox, {}, content); + }); + BrowserTestUtils.removeTab(tab); + + // Open the error page again + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank"); + browser = tab.linkedBrowser; + + loaded = BrowserTestUtils.browserLoaded(browser, true, test.frame_uri, true); + BrowserTestUtils.loadURI(browser, test.test_uri); + await loaded; + + frameBC = await SpecialPowers.spawn(browser, [], async _ => { + const iframe = content.document.getElementById("testframe"); + return iframe.browsingContext; + }); + + await SpecialPowers.spawn(frameBC, [], async _ => { + // Wait until the reporting UI is visible. + await ContentTaskUtils.waitForCondition(() => { + let reportUI = content.document.getElementById("blockingErrorReporting"); + return ContentTaskUtils.is_visible(reportUI); + }); + + let reportCheckBox = content.document.getElementById( + "automaticallyReportBlockingInFuture" + ); + is( + reportCheckBox.checked, + true, + "The checkbox of the reporting ui should be checked." + ); + + // Click on the checkbox again to disable the reporting. + await EventUtils.synthesizeMouseAtCenter(reportCheckBox, {}, content); + + is( + reportCheckBox.checked, + false, + "The checkbox of the reporting ui should be unchecked." + ); + }); + BrowserTestUtils.removeTab(tab); + + // Open the error page again to see if the reporting is disabled. + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank"); + browser = tab.linkedBrowser; + + loaded = BrowserTestUtils.browserLoaded(browser, true, test.frame_uri, true); + BrowserTestUtils.loadURI(browser, test.test_uri); + await loaded; + + frameBC = await SpecialPowers.spawn(browser, [], async _ => { + const iframe = content.document.getElementById("testframe"); + return iframe.browsingContext; + }); + + await SpecialPowers.spawn(frameBC, [], async _ => { + // Wait until the reporting UI is visible. + await ContentTaskUtils.waitForCondition(() => { + let reportUI = content.document.getElementById("blockingErrorReporting"); + return ContentTaskUtils.is_visible(reportUI); + }); + + let reportCheckBox = content.document.getElementById( + "automaticallyReportBlockingInFuture" + ); + is( + reportCheckBox.checked, + false, + "The checkbox of the reporting ui should be unchecked." + ); + }); + BrowserTestUtils.removeTab(tab); + + // Finally, check if the ping has been archived. + await new Promise(resolve => { + telemetryChecker + .promiseFindPing("xfocsp-error-report", test.expected_ping) + .then( + found => { + ok(found, "Telemetry ping submitted successfully"); + resolve(); + }, + err => { + ok(false, "Exception finding telemetry ping: " + err); + resolve(); + } + ); + }); +} diff --git a/dom/security/test/general/browser_test_toplevel_data_navigations.js b/dom/security/test/general/browser_test_toplevel_data_navigations.js new file mode 100644 index 0000000000..9219047eac --- /dev/null +++ b/dom/security/test/general/browser_test_toplevel_data_navigations.js @@ -0,0 +1,70 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +"use strict"; + +const kDataBody = "toplevel navigation to data: URI allowed"; +const kDataURI = "data:text/html,<body>" + kDataBody + "</body>"; +const kTestPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); +const kRedirectURI = kTestPath + "file_toplevel_data_navigations.sjs"; +const kMetaRedirectURI = kTestPath + "file_toplevel_data_meta_redirect.html"; + +add_task(async function test_nav_data_uri() { + await SpecialPowers.pushPrefEnv({ + set: [["security.data_uri.block_toplevel_data_uri_navigations", true]], + }); + await BrowserTestUtils.withNewTab(kDataURI, async function(browser) { + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [{ kDataBody }], + async function({ kDataBody }) { + // eslint-disable-line + is( + content.document.body.innerHTML, + kDataBody, + "data: URI navigation from system should be allowed" + ); + } + ); + }); +}); + +add_task(async function test_nav_data_uri_redirect() { + await SpecialPowers.pushPrefEnv({ + set: [["security.data_uri.block_toplevel_data_uri_navigations", true]], + }); + let tab = BrowserTestUtils.addTab(gBrowser, kRedirectURI); + registerCleanupFunction(async function() { + BrowserTestUtils.removeTab(tab); + }); + // wait to make sure data: URI did not load before checking that it got blocked + await new Promise(resolve => setTimeout(resolve, 500)); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + is( + content.document.body.innerHTML, + "", + "data: URI navigation after server redirect should be blocked" + ); + }); +}); + +add_task(async function test_nav_data_uri_meta_redirect() { + await SpecialPowers.pushPrefEnv({ + set: [["security.data_uri.block_toplevel_data_uri_navigations", true]], + }); + let tab = BrowserTestUtils.addTab(gBrowser, kMetaRedirectURI); + registerCleanupFunction(async function() { + BrowserTestUtils.removeTab(tab); + }); + // wait to make sure data: URI did not load before checking that it got blocked + await new Promise(resolve => setTimeout(resolve, 500)); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + is( + content.document.body.innerHTML, + "", + "data: URI navigation after meta redirect should be blocked" + ); + }); +}); diff --git a/dom/security/test/general/browser_test_view_image_data_navigation.js b/dom/security/test/general/browser_test_view_image_data_navigation.js new file mode 100644 index 0000000000..b7279f234b --- /dev/null +++ b/dom/security/test/general/browser_test_view_image_data_navigation.js @@ -0,0 +1,71 @@ +"use strict"; + +add_task(async function test_principal_right_click_open_link_in_new_tab() { + await SpecialPowers.pushPrefEnv({ + set: [["security.data_uri.block_toplevel_data_uri_navigations", true]], + }); + + const TEST_PAGE = + getRootDirectory(gTestPath) + "file_view_image_data_navigation.html"; + + await BrowserTestUtils.withNewTab(TEST_PAGE, async function(browser) { + let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true); + + // simulate right-click->view-image + BrowserTestUtils.waitForEvent(document, "popupshown", false, event => { + // These are operations that must be executed synchronously with the event. + document.getElementById("context-viewimage").doCommand(); + event.target.hidePopup(); + return true; + }); + BrowserTestUtils.synthesizeMouseAtCenter( + "#testimage", + { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser + ); + let tab = await loadPromise; + + let spec = tab.linkedBrowser.currentURI.spec; + ok( + spec.startsWith("data:image/svg+xml;"), + "data:image/svg navigation allowed through right-click view-image" + ); + + gBrowser.removeTab(tab); + }); +}); + +add_task(async function test_right_click_open_bg_image() { + await SpecialPowers.pushPrefEnv({ + set: [["security.data_uri.block_toplevel_data_uri_navigations", true]], + }); + + const TEST_PAGE = + getRootDirectory(gTestPath) + "file_view_bg_image_data_navigation.html"; + + await BrowserTestUtils.withNewTab(TEST_PAGE, async function(browser) { + let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true); + + // simulate right-click->view-image + BrowserTestUtils.waitForEvent(document, "popupshown", false, event => { + // These are operations that must be executed synchronously with the event. + document.getElementById("context-viewimage").doCommand(); + event.target.hidePopup(); + return true; + }); + BrowserTestUtils.synthesizeMouseAtCenter( + "#testbody", + { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser + ); + let tab = await loadPromise; + + let spec = tab.linkedBrowser.currentURI.spec; + ok( + spec.startsWith("data:image/svg+xml;"), + "data:image/svg navigation allowed through right-click view-image with background image" + ); + + gBrowser.removeTab(tab); + }); +}); diff --git a/dom/security/test/general/browser_test_xfo_embed_object.js b/dom/security/test/general/browser_test_xfo_embed_object.js new file mode 100644 index 0000000000..7332116066 --- /dev/null +++ b/dom/security/test/general/browser_test_xfo_embed_object.js @@ -0,0 +1,41 @@ +"use strict"; + +const kTestPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const kTestXFOEmbedURI = kTestPath + "file_framing_xfo_embed.html"; +const kTestXFOObjectURI = kTestPath + "file_framing_xfo_object.html"; + +const errorMessage = `The loading of “https://example.com/browser/dom/security/test/general/file_framing_xfo_embed_object.sjs†in a frame is denied by “X-Frame-Options“ directive set to “deny“`; + +let xfoBlocked = false; + +function onXFOMessage(msgObj) { + const message = msgObj.message; + + if (message.includes(errorMessage)) { + ok(true, "XFO error message logged"); + xfoBlocked = true; + } +} + +add_task(async function open_test_xfo_embed_blocked() { + xfoBlocked = false; + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + Services.console.registerListener(onXFOMessage); + BrowserTestUtils.loadURI(browser, kTestXFOEmbedURI); + await BrowserTestUtils.waitForCondition(() => xfoBlocked); + Services.console.unregisterListener(onXFOMessage); + }); +}); + +add_task(async function open_test_xfo_object_blocked() { + xfoBlocked = false; + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + Services.console.registerListener(onXFOMessage); + BrowserTestUtils.loadURI(browser, kTestXFOObjectURI); + await BrowserTestUtils.waitForCondition(() => xfoBlocked); + Services.console.unregisterListener(onXFOMessage); + }); +}); diff --git a/dom/security/test/general/bug1277803.html b/dom/security/test/general/bug1277803.html new file mode 100644 index 0000000000..c8033551a0 --- /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 0000000000..35d7e44d89 --- /dev/null +++ b/dom/security/test/general/chrome.ini @@ -0,0 +1,11 @@ +[DEFAULT] +support-files = + favicon_bug1277803.ico + bug1277803.html + +[test_innerhtml_sanitizer.html] +[test_innerhtml_sanitizer.xhtml] +[test_bug1277803.xhtml] +skip-if = os == 'android' + verify + diff --git a/dom/security/test/general/closeWindow.sjs b/dom/security/test/general/closeWindow.sjs new file mode 100644 index 0000000000..996db36f6f --- /dev/null +++ b/dom/security/test/general/closeWindow.sjs @@ -0,0 +1,24 @@ +const BODY = ` + <script> + opener.postMessage("ok!", "*"); + close(); + </script>`; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if (request.queryString.includes("unset")) { + response.setHeader("Set-Cookie", "test=wow", true); + } + + if (request.queryString.includes("none")) { + response.setHeader("Set-Cookie", "test2=wow2; samesite=none", true); + } + + if (request.queryString.includes("lax")) { + response.setHeader("Set-Cookie", "test3=wow3; samesite=lax", true); + } + + response.write(BODY); +} 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 0000000000..d44438903b --- /dev/null +++ b/dom/security/test/general/favicon_bug1277803.ico diff --git a/dom/security/test/general/file_1767581.js b/dom/security/test/general/file_1767581.js new file mode 100644 index 0000000000..259435b1e4 --- /dev/null +++ b/dom/security/test/general/file_1767581.js @@ -0,0 +1 @@ +window.testResult = "fail-script-was-loaded"; diff --git a/dom/security/test/general/file_FTP_console_warning.html b/dom/security/test/general/file_FTP_console_warning.html new file mode 100644 index 0000000000..ef711f923f --- /dev/null +++ b/dom/security/test/general/file_FTP_console_warning.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<body> +<head> + <meta charset="utf-8"> + <title>FTP subresources</title> +</head> +<body> + <iframe src="ftp://blocked.test/a.html"></iframe> + <iframe srcdoc='<iframe src="ftp://blocked.test/b.html"></iframe>'></iframe> + <iframe src='data:text/html,<iframe src="ftp://blocked.test/c.html"></iframe>'></iframe> + <img src="ftp://blocked.test/d.png"> +</body> +</html>
\ No newline at end of file diff --git a/dom/security/test/general/file_about_child.html b/dom/security/test/general/file_about_child.html new file mode 100644 index 0000000000..d83e0e4d41 --- /dev/null +++ b/dom/security/test/general/file_about_child.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1767581</title> + <script id="script" src="https://example.com/browser/dom/security/test/general/file_1767581.js"></script> +</head> +<body> + Just an about page that loads in the privileged about process! +</body> +</html>
\ No newline at end of file diff --git a/dom/security/test/general/file_assert_systemprincipal_documents.html b/dom/security/test/general/file_assert_systemprincipal_documents.html new file mode 100644 index 0000000000..2d7ff4d253 --- /dev/null +++ b/dom/security/test/general/file_assert_systemprincipal_documents.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1543579: Block web documents loading into system land </title> +</head> +<body> +<h1>This page loads documents from the SystemPrincipal (which should be blocked)</h1> +<iframe type="chrome" id="testframe" src="http://example.com/browser/dom/security/test/general/file_assert_systemprincipal_documents_iframe.html"></iframe> +</body> +</html> + diff --git a/dom/security/test/general/file_assert_systemprincipal_documents_iframe.html b/dom/security/test/general/file_assert_systemprincipal_documents_iframe.html new file mode 100644 index 0000000000..704625a1da --- /dev/null +++ b/dom/security/test/general/file_assert_systemprincipal_documents_iframe.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1543579: Block web documents loading into system land </title> +</head> +<body> +<h1>This is the iframe that should not load.</h1> +</body> +</html> 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 0000000000..0025bbfbe8 --- /dev/null +++ b/dom/security/test/general/file_block_script_wrong_mime_server.sjs @@ -0,0 +1,38 @@ +// Custom *.sjs specifically for the needs of: +// Bug 1288361 - Block scripts with wrong MIME type + +"use strict"; +Cu.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_block_subresource_redir_to_data.sjs b/dom/security/test/general/file_block_subresource_redir_to_data.sjs new file mode 100644 index 0000000000..1e312bc810 --- /dev/null +++ b/dom/security/test/general/file_block_subresource_redir_to_data.sjs @@ -0,0 +1,33 @@ +"use strict"; + +let SCRIPT_DATA = "alert('this alert should be blocked');"; +let WORKER_DATA = + "onmessage = function(event) { postMessage('worker-loaded'); }"; + +function handleRequest(request, response) { + const query = request.queryString; + + response.setHeader("Cache-Control", "no-cache", false); + response.setStatusLine("1.1", 302, "Found"); + + if (query === "script" || query === "modulescript") { + response.setHeader( + "Location", + "data:text/javascript," + escape(SCRIPT_DATA), + false + ); + return; + } + + if (query === "worker") { + response.setHeader( + "Location", + "data:text/javascript," + escape(WORKER_DATA), + false + ); + return; + } + + // we should never get here; just in case return something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/general/file_block_toplevel_data_navigation.html b/dom/security/test/general/file_block_toplevel_data_navigation.html new file mode 100644 index 0000000000..d6e083a247 --- /dev/null +++ b/dom/security/test/general/file_block_toplevel_data_navigation.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Toplevel data navigation</title> +</head> +<body> +test1: clicking data: URI tries to navigate window<br/> +<!-- postMessage will not be sent if data: URI is blocked --> +<a id="testlink" href="data:text/html,<body>toplevel data: URI navigations +should be blocked</body>">click me</a> +<script> + document.getElementById('testlink').click(); +</script> +</body> +</html> diff --git a/dom/security/test/general/file_block_toplevel_data_navigation2.html b/dom/security/test/general/file_block_toplevel_data_navigation2.html new file mode 100644 index 0000000000..957189ce07 --- /dev/null +++ b/dom/security/test/general/file_block_toplevel_data_navigation2.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Toplevel data navigation</title> +</head> +<body> +test2: data: URI in iframe tries to window.open(data:, _blank);<br/> +<iframe id="testFrame" src=""></iframe> +<script> + let DATA_URI = `data:text/html,<body><script> + var win = window.open("data:text/html,<body>toplevel data: URI navigations should be blocked</body>", "_blank"); + <\/script></body>`; + document.getElementById('testFrame').src = DATA_URI; +</script> +</body> +</html> diff --git a/dom/security/test/general/file_block_toplevel_data_navigation3.html b/dom/security/test/general/file_block_toplevel_data_navigation3.html new file mode 100644 index 0000000000..3743a72034 --- /dev/null +++ b/dom/security/test/general/file_block_toplevel_data_navigation3.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Toplevel data navigation</title> +</head> +<body> +test3: performing data: URI navigation through win.loc.href<br/> +<script> + // postMessage will not be sent if data: URI is blocked + window.location.href = "data:text/html,<body><script>" + + "window.opener.postMessage('test3','*');<\/script>toplevel data: URI " + + "navigations should be blocked</body>"; +</script> +</body> +</html> diff --git a/dom/security/test/general/file_block_toplevel_data_redirect.sjs b/dom/security/test/general/file_block_toplevel_data_redirect.sjs new file mode 100644 index 0000000000..c03ace5f23 --- /dev/null +++ b/dom/security/test/general/file_block_toplevel_data_redirect.sjs @@ -0,0 +1,13 @@ +// Custom *.sjs file specifically for the needs of Bug: +// Bug 1394554 - Block toplevel data: URI navigations after redirect + +var DATA_URI = + "<body>toplevel data: URI navigations after redirect should be blocked</body>"; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", "data:text/html," + escape(DATA_URI), false); +} diff --git a/dom/security/test/general/file_cache_splitting_isloaded.sjs b/dom/security/test/general/file_cache_splitting_isloaded.sjs new file mode 100644 index 0000000000..c48c8a3bf7 --- /dev/null +++ b/dom/security/test/general/file_cache_splitting_isloaded.sjs @@ -0,0 +1,35 @@ +/* + Helper Server - + Send a Request with ?queryResult - response will be the + queryString of the next request. + +*/ +// 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); + + // save the object state of the initial request, which returns + // async once the server has processed the img request. + if (request.queryString.includes("wait")) { + response.processAsync(); + setObjectState("wait", response); + return; + } + + response.write(IMG_BYTES); + + // return the result + getObjectState("wait", function(queryResponse) { + if (!queryResponse) { + return; + } + queryResponse.write("1"); + queryResponse.finish(); + }); +} diff --git a/dom/security/test/general/file_cache_splitting_server.sjs b/dom/security/test/general/file_cache_splitting_server.sjs new file mode 100644 index 0000000000..da75986f74 --- /dev/null +++ b/dom/security/test/general/file_cache_splitting_server.sjs @@ -0,0 +1,27 @@ +function handleRequest(request, response) { + var receivedRequests = parseInt(getState("requests")); + if (isNaN(receivedRequests)) { + receivedRequests = 0; + } + if (request.queryString.includes("state")) { + response.write(receivedRequests); + return; + } + if (request.queryString.includes("flush")) { + setState("requests", "0"); + response.write("OK"); + return; + } + response.setHeader("Cache-Control", "max-age=999999"); // Force caching + response.setHeader("Content-Type", "text/css"); + receivedRequests = receivedRequests + 1; + setState("requests", "" + receivedRequests); + response.write(` + .test{ + color:red; + } + .test h1{ + font-size:200px; + } + `); +} diff --git a/dom/security/test/general/file_cache_splitting_window.html b/dom/security/test/general/file_cache_splitting_window.html new file mode 100644 index 0000000000..59a2ff2ca9 --- /dev/null +++ b/dom/security/test/general/file_cache_splitting_window.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Document</title> + <link rel="stylesheet" href="https://example.com/tests/dom/security/test/general/file_cache_splitting_server.sjs"> +</head> +<body> + <h1>HELLO WORLD!</h1> + + <script> + window.addEventListener("load",()=>{ + fetch("file_cache_splitting_isloaded.sjs"); + }); + </script> +</body> +</html> 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 0000000000..9ee73ae3c4 --- /dev/null +++ b/dom/security/test/general/file_contentpolicytype_targeted_link_iframe.sjs @@ -0,0 +1,45 @@ +// 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_data_download.html b/dom/security/test/general/file_data_download.html new file mode 100644 index 0000000000..4cc92fe8f5 --- /dev/null +++ b/dom/security/test/general/file_data_download.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test download attribute for data: URI</title> +</head> +<body> + <a href="data:text/html,<body>data download</body>" download="data-foo.html" id="testlink">download data</a> + <script> + // click the link to have the downoad panel appear + let testlink = document.getElementById("testlink"); + testlink.click(); + </script> + </body> +</html> diff --git a/dom/security/test/general/file_data_text_csv.html b/dom/security/test/general/file_data_text_csv.html new file mode 100644 index 0000000000..a9ac369d16 --- /dev/null +++ b/dom/security/test/general/file_data_text_csv.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test open data:text/csv</title> +</head> +<body> + <a href="data:text/csv;foo,bar,foobar" id="testlink">test text/csv</a> + <script> + // click the link to have the downoad panel appear + let testlink = document.getElementById("testlink"); + testlink.click(); + </script> + </body> +</html> diff --git a/dom/security/test/general/file_framing_error_pages.sjs b/dom/security/test/general/file_framing_error_pages.sjs new file mode 100644 index 0000000000..fb62a34bdb --- /dev/null +++ b/dom/security/test/general/file_framing_error_pages.sjs @@ -0,0 +1,27 @@ +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + let query = request.queryString; + if (query === "xfo") { + response.setHeader("x-frame-options", "deny", false); + response.write("<html>xfo test loaded</html>"); + return; + } + + if (query === "csp") { + response.setHeader( + "content-security-policy", + "frame-ancestors 'none'", + false + ); + response.write("<html>csp test loaded</html>"); + 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_framing_error_pages_csp.html b/dom/security/test/general/file_framing_error_pages_csp.html new file mode 100644 index 0000000000..2764ed4aa6 --- /dev/null +++ b/dom/security/test/general/file_framing_error_pages_csp.html @@ -0,0 +1,7 @@ +<!DOCTYPE HTML> +<html> +<body> +iframe should be blocked <br/> +<iframe id="testframe" src="https://example.com/browser/dom/security/test/general/file_framing_error_pages.sjs?csp" height=800 width=800></iframe> +</body> +</html> diff --git a/dom/security/test/general/file_framing_error_pages_xfo.html b/dom/security/test/general/file_framing_error_pages_xfo.html new file mode 100644 index 0000000000..82dd1ee459 --- /dev/null +++ b/dom/security/test/general/file_framing_error_pages_xfo.html @@ -0,0 +1,7 @@ +<!DOCTYPE HTML> +<html> +<body> +iframe should be blocked <br/> +<iframe id="testframe" src="https://example.com/browser/dom/security/test/general/file_framing_error_pages.sjs?xfo" height=800 width=800></iframe> +</body> +</html> diff --git a/dom/security/test/general/file_framing_xfo_embed.html b/dom/security/test/general/file_framing_xfo_embed.html new file mode 100644 index 0000000000..f5cc761b5b --- /dev/null +++ b/dom/security/test/general/file_framing_xfo_embed.html @@ -0,0 +1,7 @@ +<!DOCTYPE HTML> +<html> +<body> + embed should be blocked <br/> + <embed src="https://example.com/browser/dom/security/test/general/file_framing_xfo_embed_object.sjs"></embed> +</body> +</html> diff --git a/dom/security/test/general/file_framing_xfo_embed_object.sjs b/dom/security/test/general/file_framing_xfo_embed_object.sjs new file mode 100644 index 0000000000..56616b7930 --- /dev/null +++ b/dom/security/test/general/file_framing_xfo_embed_object.sjs @@ -0,0 +1,7 @@ +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("x-frame-options", "deny", false); + response.write("<html>doc with x-frame-options: deny</html>"); +} diff --git a/dom/security/test/general/file_framing_xfo_object.html b/dom/security/test/general/file_framing_xfo_object.html new file mode 100644 index 0000000000..c8480a2c42 --- /dev/null +++ b/dom/security/test/general/file_framing_xfo_object.html @@ -0,0 +1,7 @@ +<!DOCTYPE HTML> +<html> +<body> + object should be blocked <br/> + <object data="https://example.com/browser/dom/security/test/general/file_framing_xfo_embed_object.sjs"></object> +</body> +</html> diff --git a/dom/security/test/general/file_gpc_server.sjs b/dom/security/test/general/file_gpc_server.sjs new file mode 100644 index 0000000000..d0b14215b4 --- /dev/null +++ b/dom/security/test/general/file_gpc_server.sjs @@ -0,0 +1,14 @@ +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + + var gpc = request.hasHeader("Sec-GPC") ? request.getHeader("Sec-GPC") : ""; + + if (gpc === "1") { + response.write("true"); + } else { + response.write("false"); + } +} diff --git a/dom/security/test/general/file_loads_nonscript.html b/dom/security/test/general/file_loads_nonscript.html new file mode 100644 index 0000000000..f7692b8066 --- /dev/null +++ b/dom/security/test/general/file_loads_nonscript.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>File that loads a non-script file-extension as script</title> +</head> +<body> + <script> + /* global equal */ + + const files = ["file_nonscript", + "file_nonscript.xyz", + "file_nonscript.html", + "file_nonscript.txt", + "file_nonscript.json"]; + + async function run() { + window.counter = 0; + + for (let file of files) { + let script = document.createElement("script"); + let promise = new Promise((resolve, reject) => { + script.addEventListener("error", resolve, {once: true}); + script.addEventListener("load", reject, {once: true}); + }); + script.src = file; + document.body.append(script); + + let event = await promise; + equal(event.type, "error"); + equal(window.counter, 0); + } + + let script = document.createElement("script"); + let promise = new Promise((resolve, reject) => { + script.addEventListener("load", resolve, {once: true}); + script.addEventListener("error", reject, {once: true}); + }); + script.src = "file_script.js"; + document.body.append(script); + + let event = await promise; + equal(event.type, "load"); + equal(window.counter, 1); + + window.postMessage("done", "*"); + } + window.addEventListener("message", run, {once: true}) + </script> +</html> diff --git a/dom/security/test/general/file_meta_referrer_in_head.html b/dom/security/test/general/file_meta_referrer_in_head.html new file mode 100644 index 0000000000..9c4c4cd695 --- /dev/null +++ b/dom/security/test/general/file_meta_referrer_in_head.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<meta name="referrer" content="no-referrer" /> +<title>Bug 1704473 - Remove head requirement for meta name=referrer</title> +<script type="application/javascript"> + fetch("https://example.com"); +</script> +</head> +<body> +</body> +</html> diff --git a/dom/security/test/general/file_meta_referrer_notin_head.html b/dom/security/test/general/file_meta_referrer_notin_head.html new file mode 100644 index 0000000000..55bd38e4c5 --- /dev/null +++ b/dom/security/test/general/file_meta_referrer_notin_head.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>Bug 1704473 - Remove head requirement for meta name=referrer</title> + +</head> +<body> + <meta name="referrer" content="no-referrer" /> + <script type="application/javascript"> + fetch("https://example.com"); + </script> +</body> +</html> diff --git a/dom/security/test/general/file_nonscript b/dom/security/test/general/file_nonscript new file mode 100644 index 0000000000..c339e45d5d --- /dev/null +++ b/dom/security/test/general/file_nonscript @@ -0,0 +1 @@ +window.counter++; diff --git a/dom/security/test/general/file_nonscript.html b/dom/security/test/general/file_nonscript.html new file mode 100644 index 0000000000..c339e45d5d --- /dev/null +++ b/dom/security/test/general/file_nonscript.html @@ -0,0 +1 @@ +window.counter++; diff --git a/dom/security/test/general/file_nonscript.json b/dom/security/test/general/file_nonscript.json new file mode 100644 index 0000000000..c339e45d5d --- /dev/null +++ b/dom/security/test/general/file_nonscript.json @@ -0,0 +1 @@ +window.counter++; diff --git a/dom/security/test/general/file_nonscript.txt b/dom/security/test/general/file_nonscript.txt new file mode 100644 index 0000000000..c339e45d5d --- /dev/null +++ b/dom/security/test/general/file_nonscript.txt @@ -0,0 +1 @@ +window.counter++; diff --git a/dom/security/test/general/file_nonscript.xyz b/dom/security/test/general/file_nonscript.xyz new file mode 100644 index 0000000000..c339e45d5d --- /dev/null +++ b/dom/security/test/general/file_nonscript.xyz @@ -0,0 +1 @@ +window.counter++; diff --git a/dom/security/test/general/file_nosniff_navigation.sjs b/dom/security/test/general/file_nosniff_navigation.sjs new file mode 100644 index 0000000000..20363ceb76 --- /dev/null +++ b/dom/security/test/general/file_nosniff_navigation.sjs @@ -0,0 +1,40 @@ +// Custom *.sjs file specifically for the needs of Bug 1286861 + +// small red image +const IMG = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +// https://stackoverflow.com/questions/17279712/what-is-the-smallest-possible-valid-pdf +const PDF = `%PDF-1.0 +1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj +trailer<</Size 4/Root 1 0 R>>`; + +function getSniffableContent(type) { + switch (type) { + case "xml": + return `<?xml version="1.0"?><test/>`; + case "html": + return `<!Doctype html> <html> <head></head> <body> Test test </body></html>`; + case "css": + return `*{ color: pink !important; }`; + case "json": + return `{ 'test':'yes' }`; + case "img": + return IMG; + case "pdf": + return PDF; + } + return "Basic UTF-8 Text"; +} + +function handleRequest(request, response) { + Cu.importGlobalProperties(["URLSearchParams"]); + let query = new URLSearchParams(request.queryString); + + // avoid confusing cache behaviors (XXXX no sure what this means?) + response.setHeader("X-Content-Type-Options", "nosniff"); // Disable Sniffing + response.setHeader("Content-Type", query.get("mime")); + response.write(getSniffableContent(query.get("content"))); +} 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 0000000000..fd35d8ad4f --- /dev/null +++ b/dom/security/test/general/file_nosniff_testserver.sjs @@ -0,0 +1,61 @@ +"use strict"; +Cu.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/file_same_site_cookies_about.sjs b/dom/security/test/general/file_same_site_cookies_about.sjs new file mode 100644 index 0000000000..421eb999be --- /dev/null +++ b/dom/security/test/general/file_same_site_cookies_about.sjs @@ -0,0 +1,99 @@ +// Custom *.sjs file specifically for the needs of Bug 1454721 + +// small red image +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +const IFRAME_INC = `<iframe src='http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_about.sjs?inclusion'></iframe>`; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // using startsWith and discard the math random + if (request.queryString.startsWith("setSameSiteCookie")) { + response.setHeader( + "Set-Cookie", + "myKey=mySameSiteAboutCookie; samesite=strict", + true + ); + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + return; + } + + // navigation tests + if (request.queryString.includes("loadsrcdocframeNav")) { + let FRAME = ` + <iframe srcdoc="foo" + onload="document.location='http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_about.sjs?navigation'"> + </iframe>`; + response.write(FRAME); + return; + } + + if (request.queryString.includes("loadblankframeNav")) { + let FRAME = ` + <iframe src="about:blank" + onload="document.location='http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_about.sjs?navigation'"> + </iframe>`; + response.write(FRAME); + return; + } + + // inclusion tets + if (request.queryString.includes("loadsrcdocframeInc")) { + response.write('<iframe srcdoc="' + IFRAME_INC + '"></iframe>'); + return; + } + + if (request.queryString.includes("loadblankframeInc")) { + let FRAME = + ` + <iframe id="blankframe" src="about:blank"></iframe> + <script> + document.getElementById("blankframe").contentDocument.write(\"` + + IFRAME_INC + + `\"); + <\/script>`; + response.write(FRAME); + return; + } + + if (request.queryString.includes("navigation")) { + const cookies = request.hasHeader("Cookie") + ? request.getHeader("Cookie") + : ""; + response.write(` + <!DOCTYPE html> + <html> + <body> + <script type="application/javascript"> + window.parent.postMessage({result: "${cookies}" }, '*'); + </script> + </body> + </html> + `); + } + + if (request.queryString.includes("inclusion")) { + const cookies = request.hasHeader("Cookie") + ? request.getHeader("Cookie") + : ""; + response.write(` + <!DOCTYPE html> + <html> + <body> + <script type="application/javascript"> + window.parent.parent.parent.postMessage({result: "${cookies}" }, '*'); + </script> + </body> + </html> + `); + } + + // we should never get here, but just in case return something unexpected + response.write("D'oh"); +} diff --git a/dom/security/test/general/file_same_site_cookies_blob_iframe_inclusion.html b/dom/security/test/general/file_same_site_cookies_blob_iframe_inclusion.html new file mode 100644 index 0000000000..b3456f0b90 --- /dev/null +++ b/dom/security/test/general/file_same_site_cookies_blob_iframe_inclusion.html @@ -0,0 +1,34 @@ +<html> +<body> +<iframe id="testframe"></iframe> +<script type="application/javascript"> + + // simply passing on the message from the child to parent + window.addEventListener("message", receiveMessage); + function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + window.parent.postMessage({result: event.data.result}, '*'); + } + + const NESTED_IFRAME_INCLUSION = ` + <html> + <body> + <script type="application/javascript"> + window.addEventListener("message", receiveMessage); + function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + window.parent.postMessage({result: event.data.result}, '*'); + } + <\/script> + <iframe src="http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_iframe.sjs"></iframe> + </body> + </html>`; + + let NESTED_BLOB_IFRAME_INCLUSION = new Blob([NESTED_IFRAME_INCLUSION], {type:'text/html'}); + + // query the testframe and set blob URL + let testframe = document.getElementById("testframe"); + testframe.src = window.URL.createObjectURL(NESTED_BLOB_IFRAME_INCLUSION); +</script> +</body> +</html> diff --git a/dom/security/test/general/file_same_site_cookies_blob_iframe_navigation.html b/dom/security/test/general/file_same_site_cookies_blob_iframe_navigation.html new file mode 100644 index 0000000000..815c6a6bfc --- /dev/null +++ b/dom/security/test/general/file_same_site_cookies_blob_iframe_navigation.html @@ -0,0 +1,30 @@ +<html> +<body> +<iframe id="testframe"></iframe> +<script type="application/javascript"> + + // simply passing on the message from the child to parent + window.addEventListener("message", receiveMessage); + function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + window.parent.postMessage({result: event.data.result}, '*'); + } + + const NESTED_IFRAME_NAVIGATION = ` + <html> + <body> + <a id="testlink" href="http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_iframe.sjs"></a> + <script type="application/javascript"> + let link = document.getElementById("testlink"); + link.click(); + <\/script> + </body> + </html>`; + let NESTED_BLOB_IFRAME_NAVIGATION = new Blob([NESTED_IFRAME_NAVIGATION], {type:'text/html'}); + + // query the testframe and set blob URL + let testframe = document.getElementById("testframe"); + testframe.src = window.URL.createObjectURL(NESTED_BLOB_IFRAME_NAVIGATION); +</script> +</body> +</html> diff --git a/dom/security/test/general/file_same_site_cookies_bug1748693.sjs b/dom/security/test/general/file_same_site_cookies_bug1748693.sjs new file mode 100644 index 0000000000..35b6a72cc9 --- /dev/null +++ b/dom/security/test/general/file_same_site_cookies_bug1748693.sjs @@ -0,0 +1,31 @@ +const MESSAGE_PAGE = function(msg) { + return ` +<!DOCTYPE html> +<html> + <body> + <p id="msg">${msg}</p> + <body> +</html> +`; +}; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-store"); + response.setHeader("Content-Type", "text/html"); + + if (request.queryString.includes("setcookies")) { + response.setHeader( + "Set-Cookie", + "auth_secure=foo; SameSite=None; HttpOnly; Secure", + true + ); + response.setHeader("Set-Cookie", "auth=foo; HttpOnly;", true); + response.write(MESSAGE_PAGE(request.queryString)); + return; + } + + const cookies = request.hasHeader("Cookie") + ? request.getHeader("Cookie") + : ""; + response.write(MESSAGE_PAGE(cookies)); +} diff --git a/dom/security/test/general/file_same_site_cookies_cross_origin_context.sjs b/dom/security/test/general/file_same_site_cookies_cross_origin_context.sjs new file mode 100644 index 0000000000..9103941653 --- /dev/null +++ b/dom/security/test/general/file_same_site_cookies_cross_origin_context.sjs @@ -0,0 +1,54 @@ +// Custom *.sjs file specifically for the needs of Bug 1452496 + +// small red image +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +const FRAME = ` + <!DOCTYPE html> + <html> + <head> + <title>Bug 1452496 - Do not allow same-site cookies in cross site context</title> + </head> + <body> + <script type="application/javascript"> + let cookie = document.cookie; + // now reset the cookie for the next test + document.cookie = "myKey=;" + "expires=Thu, 01 Jan 1970 00:00:00 GMT"; + window.parent.postMessage({result: cookie}, 'http://mochi.test:8888'); + </script> + </body> + </html>`; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if (request.queryString.includes("setSameSiteCookie")) { + response.setHeader( + "Set-Cookie", + "myKey=strictSameSiteCookie; samesite=strict", + true + ); + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + return; + } + + if (request.queryString.includes("setRegularCookie")) { + response.setHeader("Set-Cookie", "myKey=regularCookie;", true); + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + return; + } + + if (request.queryString.includes("loadFrame")) { + response.write(FRAME); + return; + } + + // we should never get here, but just in case return something unexpected + response.write("D'oh"); +} diff --git a/dom/security/test/general/file_same_site_cookies_from_script.sjs b/dom/security/test/general/file_same_site_cookies_from_script.sjs new file mode 100644 index 0000000000..0df217cf45 --- /dev/null +++ b/dom/security/test/general/file_same_site_cookies_from_script.sjs @@ -0,0 +1,48 @@ +// Custom *.sjs file specifically for the needs of Bug 1452496 + +const SET_COOKIE_FRAME = ` + <!DOCTYPE html> + <html> + <head> + <title>Bug 1452496 - Do not allow same-site cookies in cross site context</title> + </head> + <body> + <script type="application/javascript"> + document.cookie = "myKey=sameSiteCookieInlineScript;SameSite=strict"; + </script> + </body> + </html>`; + +const GET_COOKIE_FRAME = ` + <!DOCTYPE html> + <html> + <head> + <title>Bug 1452496 - Do not allow same-site cookies in cross site context</title> + </head> + <body> + <script type="application/javascript"> + let cookie = document.cookie; + // now reset the cookie for the next test + document.cookie = "myKey=;" + "expires=Thu, 01 Jan 1970 00:00:00 GMT"; + window.parent.postMessage({result: cookie}, 'http://mochi.test:8888'); + </script> + </body> + </html>`; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if (request.queryString.includes("setSameSiteCookieUsingInlineScript")) { + response.write(SET_COOKIE_FRAME); + return; + } + + if (request.queryString.includes("getCookieFrame")) { + response.write(GET_COOKIE_FRAME); + return; + } + + // we should never get here, but just in case return something unexpected + response.write("D'oh"); +} diff --git a/dom/security/test/general/file_same_site_cookies_iframe.sjs b/dom/security/test/general/file_same_site_cookies_iframe.sjs new file mode 100644 index 0000000000..7b511257c3 --- /dev/null +++ b/dom/security/test/general/file_same_site_cookies_iframe.sjs @@ -0,0 +1,99 @@ +// Custom *.sjs file specifically for the needs of Bug 1454027 + +// small red image +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +const NESTED_IFRAME_NAVIGATION = ` + <html> + <body> + <a id="testlink" href="http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_iframe.sjs"></a> + <script type="application/javascript"> + let link = document.getElementById("testlink"); + link.click(); + <\/script> + </body> + </html>`; + +const NESTED_IFRAME_INCLUSION = ` + <html> + <body> + <script type="application/javascript"> + // simply passing on the message from the child to parent + window.addEventListener("message", receiveMessage); + function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + window.parent.postMessage({result: event.data.result}, '*'); + } + <\/script> + <iframe src="http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_iframe.sjs"></iframe> + </body> + </html>`; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // using startsWith and discard the math random + if (request.queryString.startsWith("setSameSiteCookie")) { + response.setHeader( + "Set-Cookie", + "myKey=mySameSiteIframeTestCookie; samesite=strict", + true + ); + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + return; + } + + // navigation tests + if (request.queryString === "nestedIframeNavigation") { + response.write(NESTED_IFRAME_NAVIGATION); + return; + } + + if (request.queryString === "nestedSandboxIframeNavigation") { + response.setHeader( + "Content-Security-Policy", + "sandbox allow-scripts", + false + ); + response.write(NESTED_IFRAME_NAVIGATION); + return; + } + + // inclusion tests + if (request.queryString === "nestedIframeInclusion") { + response.write(NESTED_IFRAME_INCLUSION); + return; + } + + if (request.queryString === "nestedSandboxIframeInclusion") { + response.setHeader( + "Content-Security-Policy", + "sandbox allow-scripts", + false + ); + response.write(NESTED_IFRAME_INCLUSION); + return; + } + + const cookies = request.hasHeader("Cookie") + ? request.getHeader("Cookie") + : ""; + response.write(` + <!DOCTYPE html> + <html> + <head> + <title>Bug 1454027 - Update SameSite cookie handling inside iframes</title> + </head> + <body> + <script type="application/javascript"> + window.parent.postMessage({result: "${cookies}" }, '*'); + </script> + </body> + </html> + `); +} diff --git a/dom/security/test/general/file_same_site_cookies_redirect.sjs b/dom/security/test/general/file_same_site_cookies_redirect.sjs new file mode 100644 index 0000000000..f7451fb504 --- /dev/null +++ b/dom/security/test/general/file_same_site_cookies_redirect.sjs @@ -0,0 +1,103 @@ +// Custom *.sjs file specifically for the needs of Bug 1453814 + +// small red image +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +const FRAME = ` + <!DOCTYPE html> + <html> + <head> + <title>Bug 1453814 - Do not allow same-site cookies for cross origin redirect</title> + </head> + <body> + <script type="application/javascript"> + let cookie = document.cookie; + // now reset the cookie for the next test + document.cookie = "myKey=;" + "expires=Thu, 01 Jan 1970 00:00:00 GMT"; + window.parent.postMessage({result: cookie}, 'http://mochi.test:8888'); + </script> + </body> + </html>`; + +const SAME_ORIGIN = "http://mochi.test:8888/"; +const CROSS_ORIGIN = "http://example.com/"; +const PATH = + "tests/dom/security/test/general/file_same_site_cookies_redirect.sjs"; + +const FRAME_META_REFRESH_SAME = + ` + <html><head> + <meta http-equiv="refresh" content="0; + url='` + + SAME_ORIGIN + + PATH + + `?loadFrame'"> + </head></html>`; + +const FRAME_META_REFRESH_CROSS = + ` + <html><head> + <meta http-equiv="refresh" content="0; + url='` + + CROSS_ORIGIN + + PATH + + `?loadFrame'"> + </head></html>`; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if (request.queryString === "setSameSiteCookie") { + response.setHeader( + "Set-Cookie", + "myKey=strictSameSiteCookie; samesite=strict", + true + ); + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + return; + } + + if (request.queryString === "sameToSameRedirect") { + let URL = SAME_ORIGIN + PATH + "?loadFrame"; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", URL, false); + return; + } + + if (request.queryString === "sameToCrossRedirect") { + let URL = CROSS_ORIGIN + PATH + "?loadFrame"; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", URL, false); + return; + } + + if (request.queryString === "crossToSameRedirect") { + let URL = SAME_ORIGIN + PATH + "?loadFrame"; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", URL, false); + return; + } + + if (request.queryString === "sameToCrossRedirectMeta") { + response.write(FRAME_META_REFRESH_CROSS); + return; + } + + if (request.queryString === "crossToSameRedirectMeta") { + response.write(FRAME_META_REFRESH_SAME); + return; + } + + if (request.queryString === "loadFrame") { + response.write(FRAME); + return; + } + + // we should never get here, but just in case return something unexpected + response.write("D'oh"); +} diff --git a/dom/security/test/general/file_same_site_cookies_subrequest.sjs b/dom/security/test/general/file_same_site_cookies_subrequest.sjs new file mode 100644 index 0000000000..004b0c02d9 --- /dev/null +++ b/dom/security/test/general/file_same_site_cookies_subrequest.sjs @@ -0,0 +1,82 @@ +// Custom *.sjs file specifically for the needs of Bug 1286861 + +// small red image +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +const FRAME = ` + <!DOCTYPE html> + <html> + <head> + <title>Bug 1286861 - Add support for same site cookies</title> + </head> + <body> + <img src = "http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_subrequest.sjs?checkCookie"> + </body> + </html>`; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if (request.queryString.includes("setStrictSameSiteCookie")) { + response.setHeader( + "Set-Cookie", + "myKey=strictSameSiteCookie; samesite=strict", + true + ); + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + return; + } + + if (request.queryString.includes("setLaxSameSiteCookie")) { + response.setHeader( + "Set-Cookie", + "myKey=laxSameSiteCookie; samesite=lax", + true + ); + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + return; + } + + // save the object state of the initial request, which returns + // async once the server has processed the img request. + if (request.queryString.includes("queryresult")) { + response.processAsync(); + setObjectState("queryResult", response); + return; + } + + if (request.queryString.includes("loadFrame")) { + response.write(FRAME); + return; + } + + if (request.queryString.includes("checkCookie")) { + var cookie = "unitialized"; + if (request.hasHeader("Cookie")) { + cookie = request.getHeader("Cookie"); + } else { + cookie = "myKey=noCookie"; + } + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + + // return the result + getObjectState("queryResult", function(queryResponse) { + if (!queryResponse) { + return; + } + queryResponse.write(cookie); + queryResponse.finish(); + }); + return; + } + + // we should never get here, but just in case return something unexpected + response.write("D'oh"); +} diff --git a/dom/security/test/general/file_same_site_cookies_toplevel_nav.sjs b/dom/security/test/general/file_same_site_cookies_toplevel_nav.sjs new file mode 100644 index 0000000000..9bbcc09609 --- /dev/null +++ b/dom/security/test/general/file_same_site_cookies_toplevel_nav.sjs @@ -0,0 +1,96 @@ +// Custom *.sjs file specifically for the needs of Bug 1286861 + +// small red image +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +const FRAME = ` + <!DOCTYPE html> + <html> + <head> + <title>Bug 1286861 - Add support for same site cookies</title> + </head> + <body> + <script type="application/javascript"> + let myWin = window.open("http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_toplevel_nav.sjs?loadWin"); + </script> + </body> + </html>`; + +const WIN = ` + <!DOCTYPE html> + <html> + <body> + just a dummy window + <script> + window.addEventListener("load",()=>{ + window.close(); + }); + </script> + </body> + </html>`; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if (request.queryString.includes("setStrictSameSiteCookie")) { + response.setHeader( + "Set-Cookie", + "myKey=strictSameSiteCookie; samesite=strict", + true + ); + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + return; + } + + if (request.queryString.includes("setLaxSameSiteCookie")) { + response.setHeader( + "Set-Cookie", + "myKey=laxSameSiteCookie; samesite=lax", + true + ); + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + return; + } + + // save the object state of the initial request, which returns + // async once the server has processed the img request. + if (request.queryString.includes("queryresult")) { + response.processAsync(); + setObjectState("queryResult", response); + return; + } + + if (request.queryString.includes("loadFrame")) { + response.write(FRAME); + return; + } + + if (request.queryString.includes("loadWin")) { + var cookie = "unitialized"; + if (request.hasHeader("Cookie")) { + cookie = request.getHeader("Cookie"); + } else { + cookie = "myKey=noCookie"; + } + response.write(WIN); + + // return the result + getObjectState("queryResult", function(queryResponse) { + if (!queryResponse) { + return; + } + queryResponse.write(cookie); + queryResponse.finish(); + }); + return; + } + + // we should never get here, but just in case return something unexpected + response.write("D'oh"); +} diff --git a/dom/security/test/general/file_same_site_cookies_toplevel_set_cookie.sjs b/dom/security/test/general/file_same_site_cookies_toplevel_set_cookie.sjs new file mode 100644 index 0000000000..34dfe40e23 --- /dev/null +++ b/dom/security/test/general/file_same_site_cookies_toplevel_set_cookie.sjs @@ -0,0 +1,68 @@ +// Custom *.sjs file specifically for the needs of Bug 1454242 + +const WIN = ` + <html> + <body> + <script type="application/javascript"> + let newWin = window.open("http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_toplevel_set_cookie.sjs?loadWinAndSetCookie"); + newWin.onload = function() { + newWin.close(); + } + </script> + </body> + </html>`; + +const DUMMY_WIN = ` + <html> + <body> + just a dummy window that sets a same-site=lax cookie + <script type="application/javascript"> + window.opener.opener.postMessage({value: 'testSetupComplete'}, '*'); + </script> + </body> + </html>`; + +const FRAME = ` + <html> + <body> + <script type="application/javascript"> + let cookie = document.cookie; + // now reset the cookie for the next test + document.cookie = "myKey=;" + "expires=Thu, 01 Jan 1970 00:00:00 GMT"; + window.parent.postMessage({value: cookie}, 'http://mochi.test:8888'); + </script> + </body> + </html>`; + +const SAME_ORIGIN = "http://mochi.test:8888/"; +const CROSS_ORIGIN = "http://example.com/"; +const PATH = + "tests/dom/security/test/general/file_same_site_cookies_redirect.sjs"; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if (request.queryString === "loadWin") { + response.write(WIN); + return; + } + + if (request.queryString === "loadWinAndSetCookie") { + response.setHeader( + "Set-Cookie", + "myKey=laxSameSiteCookie; samesite=lax", + true + ); + response.write(DUMMY_WIN); + return; + } + + if (request.queryString === "checkCookie") { + response.write(FRAME); + return; + } + + // we should never get here, but just in case return something unexpected + response.write("D'oh"); +} diff --git a/dom/security/test/general/file_script.js b/dom/security/test/general/file_script.js new file mode 100644 index 0000000000..c339e45d5d --- /dev/null +++ b/dom/security/test/general/file_script.js @@ -0,0 +1 @@ +window.counter++; diff --git a/dom/security/test/general/file_toplevel_data_meta_redirect.html b/dom/security/test/general/file_toplevel_data_meta_redirect.html new file mode 100644 index 0000000000..e94a61ed48 --- /dev/null +++ b/dom/security/test/general/file_toplevel_data_meta_redirect.html @@ -0,0 +1,10 @@ +<html> +<body> +<head> + <meta http-equiv="refresh" + content="0; url='data:text/html,<body>toplevel meta redirect to data: URI should be blocked</body>'"> +</head> +<body> +Meta Redirect to data: URI +</body> +</html> diff --git a/dom/security/test/general/file_toplevel_data_navigations.sjs b/dom/security/test/general/file_toplevel_data_navigations.sjs new file mode 100644 index 0000000000..57c4b527dd --- /dev/null +++ b/dom/security/test/general/file_toplevel_data_navigations.sjs @@ -0,0 +1,13 @@ +// Custom *.sjs file specifically for the needs of Bug: +// Bug 1394554 - Block toplevel data: URI navigations after redirect + +var DATA_URI = + "data:text/html,<body>toplevel data: URI navigations after redirect should be blocked</body>"; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", DATA_URI, false); +} diff --git a/dom/security/test/general/file_view_bg_image_data_navigation.html b/dom/security/test/general/file_view_bg_image_data_navigation.html new file mode 100644 index 0000000000..d9aa6ca8b6 --- /dev/null +++ b/dom/security/test/general/file_view_bg_image_data_navigation.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1658244: Test navigation for right-click view-bg-image on data:image/svg</title> +<style> +body { + background: fixed #222 url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjUwMCIgd2lkdGg9IjUwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGZpbHRlciBpZD0iYSI+PGZlVHVyYnVsZW5jZSBiYXNlRnJlcXVlbmN5PSIuOSIgbnVtT2N0YXZlcz0iMTAiIHN0aXRjaFRpbGVzPSJzdGl0Y2giIHR5cGU9ImZyYWN0YWxOb2lzZSIvPjwvZmlsdGVyPjxwYXRoIGQ9Im0wIDBoNTAwdjUwMGgtNTAweiIgZmlsbD0iIzExMSIvPjxwYXRoIGQ9Im0wIDBoNTAwdjUwMGgtNTAweiIgZmlsdGVyPSJ1cmwoI2EpIiBvcGFjaXR5PSIuMiIvPjwvc3ZnPgo="); + color: #ccc; +} +</style> +</head> +<body id="testbody"> + This page has an inline SVG image as a background. +</body> +</html> diff --git a/dom/security/test/general/file_view_image_data_navigation.html b/dom/security/test/general/file_view_image_data_navigation.html new file mode 100644 index 0000000000..a3f9acfb4d --- /dev/null +++ b/dom/security/test/general/file_view_image_data_navigation.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1407891: Test navigation for right-click view-image on data:image/svg</title> +</head> +<body> + +<img id="testimage" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNOCwxMkwzLDcsNCw2bDQsNCw0LTQsMSwxWiIgZmlsbD0iIzZBNkE2QSIgLz4KPC9zdmc+Cg=="></img> + +</body> +</html> diff --git a/dom/security/test/general/file_xfo_error_page.sjs b/dom/security/test/general/file_xfo_error_page.sjs new file mode 100644 index 0000000000..b1fa33cbd4 --- /dev/null +++ b/dom/security/test/general/file_xfo_error_page.sjs @@ -0,0 +1,8 @@ +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + response.setHeader("x-frame-options", "deny", false); + response.write("<html>xfo test loaded</html>"); +} diff --git a/dom/security/test/general/mochitest.ini b/dom/security/test/general/mochitest.ini new file mode 100644 index 0000000000..d315d1f50c --- /dev/null +++ b/dom/security/test/general/mochitest.ini @@ -0,0 +1,69 @@ +[DEFAULT] +support-files = + file_contentpolicytype_targeted_link_iframe.sjs + file_nosniff_testserver.sjs + file_nosniff_navigation.sjs + file_block_script_wrong_mime_server.sjs + file_block_toplevel_data_navigation.html + file_block_toplevel_data_navigation2.html + file_block_toplevel_data_navigation3.html + file_block_toplevel_data_redirect.sjs + file_block_subresource_redir_to_data.sjs + file_same_site_cookies_subrequest.sjs + file_same_site_cookies_toplevel_nav.sjs + file_same_site_cookies_cross_origin_context.sjs + file_same_site_cookies_from_script.sjs + file_same_site_cookies_redirect.sjs + file_same_site_cookies_toplevel_set_cookie.sjs + file_same_site_cookies_blob_iframe_navigation.html + file_same_site_cookies_blob_iframe_inclusion.html + file_same_site_cookies_iframe.sjs + file_same_site_cookies_about.sjs + file_cache_splitting_server.sjs + file_cache_splitting_isloaded.sjs + file_cache_splitting_window.html + window_nosniff_navigation.html + +[test_contentpolicytype_targeted_link_iframe.html] +[test_nosniff.html] +[test_cache_split.html] +[test_nosniff_navigation.html] +[test_block_script_wrong_mime.html] +[test_block_toplevel_data_navigation.html] +[test_block_toplevel_data_img_navigation.html] +[test_allow_opening_data_pdf.html] +skip-if = toolkit == 'android' # no pdf reader on Android +[test_allow_opening_data_json.html] +[test_block_subresource_redir_to_data.html] +[test_same_site_cookies_subrequest.html] +fail-if = xorigin # Cookies set incorrectly +[test_same_site_cookies_toplevel_nav.html] +fail-if = xorigin +[test_same_site_cookies_cross_origin_context.html] +[test_same_site_cookies_from_script.html] +fail-if = xorigin +[test_same_site_cookies_redirect.html] +fail-if = xorigin +[test_same_site_cookies_toplevel_set_cookie.html] +fail-if = xorigin # Cookies not set +[test_same_site_cookies_iframe.html] +fail-if = xorigin +[test_same_site_cookies_about.html] +fail-if = xorigin +[test_assert_about_page_no_csp.html] +skip-if = !debug +[test_same_site_cookies_laxByDefault.html] +skip-if = debug +support-files = closeWindow.sjs +[test_xfo_error_page.html] +support-files = file_xfo_error_page.sjs +[test_bug1450853.html] +[test_meta_referrer.html] +support-files = + file_meta_referrer_in_head.html + file_meta_referrer_notin_head.html +[test_bug1660452_http.html] +[test_bug1660452_https.html] +scheme = https +[test_gpc.html] +support-files = file_gpc_server.sjs diff --git a/dom/security/test/general/test_allow_opening_data_json.html b/dom/security/test/general/test_allow_opening_data_json.html new file mode 100644 index 0000000000..4b37931e1f --- /dev/null +++ b/dom/security/test/general/test_allow_opening_data_json.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1403814: Allow toplevel data URI navigation data:application/json</title> + <script 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"> + +SimpleTest.waitForExplicitFinish(); + +function test_toplevel_data_json() { + const DATA_JSON = "data:application/json,{'my_json_key':'my_json_value'}"; + + let win = window.open(DATA_JSON); + let wrappedWin = SpecialPowers.wrap(win); + + // Unfortunately we can't detect whether the JSON has loaded or not using some + // event, hence we are constantly polling location.href till we see that + // the data: URI appears. Test times out on failure. + var jsonLoaded = setInterval(function() { + if (wrappedWin.document.location.href.startsWith("data:application/json")) { + clearInterval(jsonLoaded); + ok(true, "navigating to data:application/json allowed"); + wrappedWin.close(); + SimpleTest.finish(); + } + }, 200); +} + +SpecialPowers.pushPrefEnv({ + set: [["security.data_uri.block_toplevel_data_uri_navigations", true]] +}, test_toplevel_data_json); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_allow_opening_data_pdf.html b/dom/security/test/general/test_allow_opening_data_pdf.html new file mode 100644 index 0000000000..007b3e8801 --- /dev/null +++ b/dom/security/test/general/test_allow_opening_data_pdf.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1398692: Allow toplevel navigation to a data:application/pdf</title> + <script 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"> + +SimpleTest.waitForExplicitFinish(); + +function test_toplevel_data_pdf() { + // The PDF contains one page and it is a 3/72" square, the minimum allowed by the spec + const DATA_PDF = + "data:application/pdf;base64,JVBERi0xLjANCjEgMCBvYmo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PmVuZG9iaiAyIDAgb2JqPDwvVHlwZS9QYWdlcy9LaWRzWzMgMCBSXS9Db3VudCAxPj5lbmRvYmogMyAwIG9iajw8L1R5cGUvUGFnZS9NZWRpYUJveFswIDAgMyAzXT4+ZW5kb2JqDQp4cmVmDQowIDQNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAxMCAwMDAwMCBuDQowMDAwMDAwMDUzIDAwMDAwIG4NCjAwMDAwMDAxMDIgMDAwMDAgbg0KdHJhaWxlcjw8L1NpemUgNC9Sb290IDEgMCBSPj4NCnN0YXJ0eHJlZg0KMTQ5DQolRU9G"; + + let win = window.open(DATA_PDF); + let wrappedWin = SpecialPowers.wrap(win); + + // Unfortunately we can't detect whether the PDF has loaded or not using some + // event, hence we are constantly polling location.href till we see that + // the data: URI appears. Test times out on failure. + var pdfLoaded = setInterval(function() { + if (wrappedWin.document.location.href.startsWith("data:application/pdf")) { + clearInterval(pdfLoaded); + ok(true, "navigating to data:application/pdf allowed"); + wrappedWin.close(); + SimpleTest.finish(); + } + }, 200); +} + +SpecialPowers.pushPrefEnv({ + set: [["security.data_uri.block_toplevel_data_uri_navigations", true]] +}, test_toplevel_data_pdf); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_assert_about_page_no_csp.html b/dom/security/test/general/test_assert_about_page_no_csp.html new file mode 100644 index 0000000000..06be4ce460 --- /dev/null +++ b/dom/security/test/general/test_assert_about_page_no_csp.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1490977: Test Assertion if content privileged about: page has no CSP</title> + <script 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"> + + // Test Setup: The test overrules the allowlist of about: pages that are allowed to load + // without a CSP and makes sure to hit the assertion within AssertAboutPageHasCSP(). + + SpecialPowers.setBoolPref("dom.security.skip_about_page_csp_allowlist_and_assert", true); + + SimpleTest.waitForExplicitFinish(); + SimpleTest.expectAssertions(0, 1); + + ok(true, "sanity: prefs flipped and test runs"); + let myFrame = document.getElementById("testframe"); + myFrame.src = "about:blank"; + // booom :-) + + SpecialPowers.setBoolPref("dom.security.skip_about_page_csp_allowlist_and_assert", false); + SimpleTest.finish(); +</script> +</pre> +</body> +</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 0000000000..93a4b9d220 --- /dev/null +++ b/dom/security/test/general/test_block_script_wrong_mime.html @@ -0,0 +1,92 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1288361 - Block scripts with incorrect MIME type</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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`); + 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`); + error.preventDefault(); + resolve(); + } + worker.postMessage("dummy"); + }); +} + +SimpleTest.waitForExplicitFinish(); +Promise.all(MIMETypes.map(testScript)).then(() => { + return Promise.all(MIMETypes.map(testWorker)); +}).then(() => { + return Promise.all(MIMETypes.map(testWorkerImportScripts)); +}).then(() => { + return SpecialPowers.popPrefEnv(); +}).then(SimpleTest.finish); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_block_subresource_redir_to_data.html b/dom/security/test/general/test_block_subresource_redir_to_data.html new file mode 100644 index 0000000000..eafb6b5d83 --- /dev/null +++ b/dom/security/test/general/test_block_subresource_redir_to_data.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1428793: Block insecure redirects to data: URIs</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script id="testScriptRedirectToData"></script> +<script id="testModuleScriptRedirectToData" type="module"></script> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +const NUM_TESTS = 3; + +var testCounter = 0; +function checkFinish() { + testCounter++; + if (testCounter === NUM_TESTS) { + SimpleTest.finish(); + } +} + +// --- test regular scripts +let testScriptRedirectToData = document.getElementById("testScriptRedirectToData"); +testScriptRedirectToData.onerror = function() { + ok(true, "script that redirects to data: URI should not load"); + checkFinish(); +} +testScriptRedirectToData.onload = function() { + ok(false, "script that redirects to data: URI should not load"); + checkFinish(); +} +testScriptRedirectToData.src = "file_block_subresource_redir_to_data.sjs?script"; + +// --- test workers +let worker = new Worker("file_block_subresource_redir_to_data.sjs?worker"); +worker.onerror = function() { + // please note that workers need to be same origin, hence the data: URI + // redirect is blocked by worker code and not the content security manager! + ok(true, "worker script that redirects to data: URI should not load"); + checkFinish(); +} +worker.onmessage = function() { + ok(false, "worker script that redirects to data: URI should not load"); + checkFinish(); +}; +worker.postMessage("dummy"); + +// --- test script modules +SpecialPowers.pushPrefEnv({set: [["dom.moduleScripts.enabled", true]]}, function() { + let testModuleScriptRedirectToData = document.getElementById("testModuleScriptRedirectToData"); + testModuleScriptRedirectToData.onerror = function() { + ok(true, "module script that redirects to data: URI should not load"); + checkFinish(); + } + testModuleScriptRedirectToData.onload = function() { + ok(false, "module script that redirects to data: URI should not load"); + checkFinish(); + } + testModuleScriptRedirectToData.src = "file_block_subresource_redir_to_data.sjs?modulescript"; +}); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_block_toplevel_data_img_navigation.html b/dom/security/test/general/test_block_toplevel_data_img_navigation.html new file mode 100644 index 0000000000..07e46b1f2f --- /dev/null +++ b/dom/security/test/general/test_block_toplevel_data_img_navigation.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1396798: Do not block toplevel data: navigation to image (except svgs)</title> + <script 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"> +SpecialPowers.setBoolPref("security.data_uri.block_toplevel_data_uri_navigations", true); +SimpleTest.registerCleanupFunction(() => { + SpecialPowers.clearUserPref("security.data_uri.block_toplevel_data_uri_navigations"); +}); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("have to test that top level data:image loading is blocked/allowed"); + +function test_toplevel_data_image() { + const DATA_PNG = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; + let win1 = window.open(DATA_PNG); + let wrappedWin1 = SpecialPowers.wrap(win1); + setTimeout(function () { + let images = wrappedWin1.document.getElementsByTagName('img'); + is(images.length, 1, "Loading data:image/png should be allowed"); + is(images[0].src, DATA_PNG, "Sanity: img src matches"); + wrappedWin1.close(); + test_toplevel_data_image_svg(); + }, 1000); +} + +function test_toplevel_data_image_svg() { + const DATA_SVG = + "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNOCwxMkwzLDcsNCw2bDQsNCw0LTQsMSwxWiIgZmlsbD0iIzZBNkE2QSIgLz4KPC9zdmc+Cg=="; + let win2 = window.open(DATA_SVG); + // Unfortunately we can't detect whether the window was closed using some event, + // hence we are constantly polling till we see that win == null. + // Test times out on failure. + var win2Closed = setInterval(function() { + if (win2 == null || win2.closed) { + clearInterval(win2Closed); + ok(true, "Loading data:image/svg+xml should be blocked"); + SimpleTest.finish(); + } + }, 200); +} +// fire up the tests +test_toplevel_data_image(); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_block_toplevel_data_navigation.html b/dom/security/test/general/test_block_toplevel_data_navigation.html new file mode 100644 index 0000000000..d61b6c7110 --- /dev/null +++ b/dom/security/test/general/test_block_toplevel_data_navigation.html @@ -0,0 +1,133 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1331351 - Block top level window data: URI navigations</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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"> + +async function expectBlockedToplevelData() { + await SpecialPowers.spawnChrome([], async () => { + let progressListener; + let bid = await new Promise(resolve => { + let bcs = []; + progressListener = { + QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener", "nsISupportsWeakReference"]), + onStateChange(webProgress, request, stateFlags, status) { + if (!(request instanceof Ci.nsIChannel) || !webProgress.isTopLevel || + !(stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) || + !(stateFlags & Ci.nsIWebProgressListener.STATE_STOP)) { + return; + } + + if (ChromeUtils.getXPCOMErrorName(status) != "NS_ERROR_DOM_BAD_URI") { + isnot(request.URI.scheme, "data"); + return; + } + + // We can't check for the scheme to be "data" because in the case of a + // redirected load, we'll get a `NS_ERROR_DOM_BAD_URI` load error + // before observing the redirect, cancelling the load. Instead we just + // wait for any load to error with `NS_ERROR_DOM_BAD_URI`. + for (let bc of bcs) { + try { + bc.webProgress.removeProgressListener(progressListener); + } catch(e) { } + } + bcs = []; + Services.obs.removeObserver(observer, "browsing-context-attached"); + resolve(webProgress.browsingContext.browserId); + } + }; + + function observer(subject, topic) { + if (!bcs.includes(subject.webProgress)) { + bcs.push(subject.webProgress); + subject.webProgress.addProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_ALL); + } + } + Services.obs.addObserver(observer, "browsing-context-attached"); + }); + return bid; + }); +} + +async function expectBlockedURIWarning() { + await SpecialPowers.spawnChrome([], async () => { + return new Promise(resolve => { + Services.console.registerListener(function onConsoleMessage(msg) { + info("Seeing console message: " + msg.message); + if (!(msg instanceof Ci.nsIScriptError)) { + return; + } + if (msg.category != "DATA_URI_BLOCKED") { + return; + } + + Services.console.unregisterListener(onConsoleMessage); + resolve(); + }); + }); + }); +} + +async function expectBrowserDiscarded(browserId) { + await SpecialPowers.spawnChrome([browserId], async (browserId) => { + return new Promise(resolve => { + function check() { + if (!BrowsingContext.getCurrentTopByBrowserId(browserId)) { + ok(true, `BrowserID ${browserId} discarded`); + resolve(); + Services.obs.removeObserver(check, "browsing-context-discarded"); + } + } + Services.obs.addObserver(check, "browsing-context-discarded"); + check(); + }); + }); +} + +async function popupTest(uri, expectClose) { + info(`Running expect blocked test for ${uri}`); + let reqBlockedPromise = expectBlockedToplevelData(); + let warningPromise = expectBlockedURIWarning(); + let win = window.open(uri); + let browserId = await reqBlockedPromise; + await warningPromise; + if (expectClose) { + await expectBrowserDiscarded(browserId); + } + win.close(); +} + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [["security.data_uri.block_toplevel_data_uri_navigations", true]], + }); + + // simple data: URI click navigation should be prevented + await popupTest("file_block_toplevel_data_navigation.html", false); + + // data: URI in iframe which opens data: URI in _blank should be blocked + await popupTest("file_block_toplevel_data_navigation2.html", false); + + // navigating to a data: URI using window.location.href should be blocked + await popupTest("file_block_toplevel_data_navigation3.html", false); + + // navigating to a data: URI using window.open() should be blocked + await popupTest("data:text/html,<body>toplevel data: URI navigations should be blocked</body>", false); + + // navigating to a URI which redirects to a data: URI using window.open() should be blocked + await popupTest("file_block_toplevel_data_redirect.sjs", false); + + // navigating to a data: URI without a Content Type should be blocked + await popupTest("data:,DataURIsWithNoContentTypeShouldBeBlocked", false); +}); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_bug1277803.xhtml b/dom/security/test/general/test_bug1277803.xhtml new file mode 100644 index 0000000000..30cc82310b --- /dev/null +++ b/dom/security/test/general/test_bug1277803.xhtml @@ -0,0 +1,65 @@ +<?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 src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + </body> + + <script type="application/javascript"><![CDATA[ + SimpleTest.requestCompleteLog(); + + 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 expectedPrincipal = Services.scriptSecurityManager + .createContentPrincipal(Services.io.newURI(LOADING_URI), {}); + let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + + function runTest() { + // Register our observer to intercept favicon requests. + function observer(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; + + ok(loadingPrincipal.equals(expectedPrincipal), "Should be loading with the expected principal."); + ok(triggeringPrincipal.equals(expectedPrincipal), "Should be triggered with the expected principal."); + + Services.obs.removeObserver(this, "http-on-modify-request"); + SimpleTest.finish(); + } + Services.obs.addObserver(observer, "http-on-modify-request"); + + // 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="true" flex="1" id="content" src="about:blank"/> +</window> diff --git a/dom/security/test/general/test_bug1450853.html b/dom/security/test/general/test_bug1450853.html new file mode 100644 index 0000000000..e6b61ecce0 --- /dev/null +++ b/dom/security/test/general/test_bug1450853.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1450853 +--> +<head> +<meta charset="utf-8"> +<title>Test for Cross-origin resouce status leak via MediaError</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/ChromeTask.js"></script> +<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + +<audio autoplay id="audio"></audio> + +<script type="application/javascript"> + +/** Test for Bug 1450853 **/ +var CONST_GENERIC_ERROR_MESSAGE = "Failed to open media"; + +add_task(function() { + return new Promise((resolve) => { + let audioElement = document.getElementById("audio"); + + audioElement.onerror = function() { + let err = this.error; + let message = err.message; + + info(`Got Audio Error -> ${message}`); + ok(message.includes("404"), "Same-Origin Error Message should contain status data"); + resolve(); + }; + audioElement.src = "http://mochi.test:8888/media/test.mp3"; + }); +}); + +add_task(function() { + return new Promise((resolve) => { + let audioElement = document.getElementById("audio"); + + audioElement.onerror = function() { + let err = this.error; + let message = err.message; + + info(`Got Audio Error -> ${message}`); + is(message, CONST_GENERIC_ERROR_MESSAGE, "Cross-Origin Same-Site Error Message should be generic"); + resolve(); + }; + audioElement.src = "http://mochi.test:9999/media/test.mp3"; + }); +}); + +add_task(function() { + return new Promise((resolve) => { + let audioElement = document.getElementById("audio"); + + audioElement.onerror = function() { + let err = this.error; + let message = err.message; + + info(`Got Audio Error -> ${message}`); + is(message, CONST_GENERIC_ERROR_MESSAGE, "Cross-Origin Same-Site Error Message should be generic"); + resolve(); + }; + audioElement.src = "http://sub.mochi.test:8888/media/test.mp3"; + }); +}); + +add_task(function() { + return new Promise((resolve) => { + let audioElement = document.getElementById("audio"); + + audioElement.onerror = function() { + let err = this.error; + let message = err.message; + + info(`Got Audio Error -> ${message}`); + is(message, CONST_GENERIC_ERROR_MESSAGE, "Cross-Origin Error Message should be generic"); + resolve(); + }; + audioElement.src = "https://example.com/media/test.mp3"; + }); +}); + +</script> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1450853">Mozilla Bug 1450853</a> + <iframe width="0" height="0"></iframe> + </body> +</html> diff --git a/dom/security/test/general/test_bug1660452_http.html b/dom/security/test/general/test_bug1660452_http.html new file mode 100644 index 0000000000..3a6512da21 --- /dev/null +++ b/dom/security/test/general/test_bug1660452_http.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1660452: NullPrincipals need to know whether they were spun off of a Secure Context</title> +<script 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"> + +SimpleTest.waitForExplicitFinish(); +ok(!window.isSecureContext, "top level should not be a secure context"); + +// eslint-disable-next-line +let newWin = window.open("data:text/html,<script><"+"/script>"); +ok(!newWin.isSecureContext, "data uri window should not be a secure context"); +newWin.close(); + +window.addEventListener("message", (event) => { + ok(!event.data.isSecureContext, "data uri frames should not be a secure context"); + if(event.data.finish) { + SimpleTest.finish(); + return; + } + let f2 = document.createElement("iframe"); + // eslint-disable-next-line + f2.src = "data:text/html,<iframe src=\"data:text/html,<script>parent.parent.postMessage({isSecureContext: window.isSecureContext, finish: true}, '*');<"+"/script>\"></iframe>"; + document.body.appendChild(f2); +}); + +let f = document.createElement("iframe"); +// eslint-disable-next-line +f.src = "data:text/html,<script>parent.postMessage({isSecureContext: window.isSecureContext}, '*');<"+"/script>"; +document.body.appendChild(f); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_bug1660452_https.html b/dom/security/test/general/test_bug1660452_https.html new file mode 100644 index 0000000000..1aed356a21 --- /dev/null +++ b/dom/security/test/general/test_bug1660452_https.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1660452: NullPrincipals need to know whether they were spun off of a Secure Context</title> +<script 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"> + +SimpleTest.waitForExplicitFinish(); +ok(window.isSecureContext, "top level should be a secure context"); + +// eslint-disable-next-line +let newWin = window.open("data:text/html,<script><"+"/script>"); +ok(newWin.isSecureContext, "data uri window should be a secure context"); +newWin.close(); + +window.addEventListener("message", (event) => { + ok(event.data.isSecureContext, "data uri frames should be a secure context"); + if(event.data.finish) { + SimpleTest.finish(); + return; + } + let f2 = document.createElement("iframe"); + // eslint-disable-next-line + f2.src = "data:text/html,<iframe src=\"data:text/html,<script>parent.parent.postMessage({isSecureContext: window.isSecureContext, finish: true}, '*');<"+"/script>\"></iframe>"; + document.body.appendChild(f2); +}); + +let f = document.createElement("iframe"); +// eslint-disable-next-line +f.src = "data:text/html,<script>parent.postMessage({isSecureContext: window.isSecureContext}, '*');<"+"/script>"; +document.body.appendChild(f); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_cache_split.html b/dom/security/test/general/test_cache_split.html new file mode 100644 index 0000000000..f0fc056bce --- /dev/null +++ b/dom/security/test/general/test_cache_split.html @@ -0,0 +1,153 @@ +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>Bug 1454721 - Add same-site cookie test for about:blank and about:srcdoc</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ChromeTask.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + <img id="cookieImage"> + <script class="testbody" type="text/javascript"> + SimpleTest.requestLongerTimeout(2); + + const CROSS_ORIGIN = "http://mochi.test:8888/"; + const SAME_ORIGIN= "https://example.com/"; + const PATH = "file_cache_splitting_server.sjs"; + + async function getCount() { + return fetch(`${PATH}?state`).then(r => r.text()); + } + async function resetCount() { + return fetch(`${PATH}?flush`).then(r => r.text()); + } + async function ensureLoaded() { + // This Fetch is geting the Response "1", once file_cache_splitting_isloaded + // gets a request without a query String issued from the cache_splitting_window.html + info("Waiting for Pageload"); + let result = await fetch("file_cache_splitting_isloaded.sjs?wait").then(r => r.text); + info("Page has been Loaded"); + return result; + } + + + async function openAndLoadWindow(origin) { + let isLoaded = ensureLoaded(); + let url = `${origin}tests/dom/security/test/general/file_cache_splitting_window.html`; + let w = window.open(url); + // let ew = SpecialPowers.wrap(w); + await isLoaded; + return w; + } + + async function checkStep(step = [SAME_ORIGIN, 1], name) { + info(`Doing Step ${JSON.stringify(step)}`); + let url = step[0]; + let should_count = step[1]; + let w = await openAndLoadWindow(url); + let count = await getCount(); + ok( + count == should_count, + `${name} req to: ${ + url == SAME_ORIGIN ? "Same Origin" : "Cross Origin" + } expected ${should_count} request to Server, got ${count}` + ); + w.close() + } + async function clearCache(){ + info("Clearing Cache"); + SpecialPowers.DOMWindowUtils.clearSharedStyleSheetCache(); + await ChromeTask.spawn(null,(()=>{ + Services.cache2.clear(); + })); + } + async function runTest(test) { + info(`Starting Job with - ${test.steps.length} - Requests`); + await resetCount(); + let { prefs, steps, name } = test; + await SpecialPowers.pushPrefEnv(prefs); + for (let step of steps) { + await checkStep(step, name); + } + await clearCache(); + }; + + + add_task( + async () => + runTest({ + name: `Isolated Cache`, + steps: [[SAME_ORIGIN, 1], [SAME_ORIGIN, 1], [CROSS_ORIGIN, 2]], + prefs: { + set: [ + ["privacy.partition.network_state", true] + ], + }, + }) + ); + // Negative Test: The CROSS_ORIGIN should be able to + // acess the cache of SAME_ORIGIN + add_task( + async () => + runTest({ + name: `Non Isolated Cache`, + steps: [[SAME_ORIGIN, 1], [SAME_ORIGIN, 1], [CROSS_ORIGIN, 1]], + prefs: { + set: [ + ["privacy.partition.network_state", false] + ], + }, + }) + ); + // Test that FPI does not affect Cache Isolation + add_task( + async () => + runTest({ + name: `FPI interaction`, + steps: [[SAME_ORIGIN, 1], [SAME_ORIGIN, 1], [CROSS_ORIGIN, 2]], + prefs: { + set: [ + ["privacy.firstparty.isolate", true], + ["privacy.partition.network_state", false], + ], + }, + }) + ); + // Test that cookieBehavior does not affect Cache Isolation + for (let i = 0; i < SpecialPowers.Ci.nsICookieService.BEHAVIOR_LAST ; i++) { + add_task( + async () => + runTest({ + name: `cookieBehavior interaction ${i}`, + steps: [[SAME_ORIGIN, 1], [SAME_ORIGIN, 1], [CROSS_ORIGIN, 2]], + prefs: { + set: [ + ["privacy.firstparty.isolate", false], + ["network.cookie.cookieBehavior", i], + ["privacy.partition.network_state", true], + ], + }, + }) + ); + } + add_task( + async () => + runTest({ + name: `FPI interaction - 2`, + steps: [[SAME_ORIGIN, 1], [SAME_ORIGIN, 1], [CROSS_ORIGIN, 2]], + prefs: { + set: [ + ["privacy.firstparty.isolate", true], + ["privacy.partition.network_state", false], + ], + }, + }) + ); + + </script> +</body> + +</html> 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 0000000000..99620d06f9 --- /dev/null +++ b/dom/security/test/general/test_contentpolicytype_targeted_link_iframe.html @@ -0,0 +1,103 @@ +<!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 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. + */ + +function createChromeScript() { + /* eslint-env mozilla/chrome-script */ + const POLICYNAME = "@mozilla.org/testpolicy;1"; + const POLICYID = Components.ID("{6cc95ef3-40e1-4d59-87f0-86f100373227}"); + const EXPECTED_URL = + "http://mochi.test:8888/tests/dom/security/test/general/file_contentpolicytype_targeted_link_iframe.sjs?innerframe"; + + var policy = { + // nsISupports implementation + QueryInterface: ChromeUtils.generateQI([ + "nsIFactory", + "nsIContentPolicy", + ]), + + // nsIFactory implementation + createInstance(iid) { + return this.QueryInterface(iid); + }, + + // nsIContentPolicy implementation + shouldLoad(contentLocation, loadInfo, mimeTypeGuess) { + if (contentLocation.asciiSpec === EXPECTED_URL) { + sendAsyncMessage("loadBlocked", { policyType: loadInfo.externalContentPolicyType}); + Services.catMan.deleteCategoryEntry( + "content-policy", + POLICYNAME, + false + ); + componentManager.unregisterFactory(POLICYID, policy); + return Ci.nsIContentPolicy.REJECT_REQUEST; + } + return Ci.nsIContentPolicy.ACCEPT; + }, + + shouldProcess(contentLocation, loadInfo, mimeTypeGuess) { + return Ci.nsIContentPolicy.ACCEPT; + } + }; + + // Register content policy + var componentManager = Components.manager.QueryInterface( + Ci.nsIComponentRegistrar + ); + + componentManager.registerFactory( + POLICYID, + "Test content policy", + POLICYNAME, + policy + ); + Services.catMan.addCategoryEntry( + "content-policy", + POLICYNAME, + POLICYNAME, + false, + true + ); + + // Adding a new category dispatches an event to update + // caches, so we need to also dispatch an event to make + // sure we don't start the load until after that happens. + Services.tm.dispatchToMainThread(() => { + sendAsyncMessage("setupComplete"); + }); +} + +add_task(async function() { + let chromeScript = SpecialPowers.loadChromeScript(createChromeScript); + await chromeScript.promiseOneMessage("setupComplete"); + + var testframe = document.getElementById("testframe"); + testframe.src = + "file_contentpolicytype_targeted_link_iframe.sjs?testframe"; + + let result = await chromeScript.promiseOneMessage("loadBlocked"); + + is(result.policyType, SpecialPowers.Ci.nsIContentPolicy.TYPE_SUBDOCUMENT, + "content policy type should TYPESUBDOCUMENT"); + + chromeScript.destroy(); +}); +</script> +</body> +</html> diff --git a/dom/security/test/general/test_gpc.html b/dom/security/test/general/test_gpc.html new file mode 100644 index 0000000000..7efccef0d8 --- /dev/null +++ b/dom/security/test/general/test_gpc.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Global Privacy Control headers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="application/javascript"> + +add_task(async function testGlobalPrivacyControlDisabled() { + await SpecialPowers.pushPrefEnv({ set: [ + ["privacy.globalprivacycontrol.enabled", false], + ["privacy.globalprivacycontrol.functionality.enabled", true], + ]}); + await fetch("file_gpc_server.sjs") + .then((response) => response.text()) + .then((response) => { + is(response, "false", "GPC disabled so header unsent"); + is(navigator.globalPrivacyControl, false, "GPC disabled so navigator property is 0"); + }); +}); + +add_task(async function testGlobalPrivacyControlEnabled() { + await SpecialPowers.pushPrefEnv({ set: [ + ["privacy.globalprivacycontrol.enabled", true], + ["privacy.globalprivacycontrol.functionality.enabled", true], + ]}); + await fetch("file_gpc_server.sjs") + .then((response) => response.text()) + .then((response) => { + is(response, "true", "GPC enabled so header sent and received"); + is(navigator.globalPrivacyControl, true, "GPC enabled so navigator property is 1"); + }); +}); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_innerhtml_sanitizer.html b/dom/security/test/general/test_innerhtml_sanitizer.html new file mode 100644 index 0000000000..4a4e4efed1 --- /dev/null +++ b/dom/security/test/general/test_innerhtml_sanitizer.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset=utf-8> + <title>Test for Bug 1667113</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1667113">Mozilla Bug 1667113</a> +<div></div> +<script> +SimpleTest.waitForExplicitFinish(); + +// Please note that 'fakeServer' does not exist because the test relies +// on "csp-on-violate-policy" , and "specialpowers-http-notify-request" +// which fire if either the request is blocked or fires. The test does +// not rely on the result of the load. + +function fail() { + ok(false, "Should not call this") +} + +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(subject, topic, data) { + if (topic === "csp-on-violate-policy") { + let asciiSpec = SpecialPowers.getPrivilegedProps( + SpecialPowers.do_QueryInterface(subject, "nsIURI"), + "asciiSpec"); + if (asciiSpec.includes("fakeServer")) { + ok (false, "Should not attempt fetch, not even blocked by CSP."); + } + } + + if (topic === "specialpowers-http-notify-request") { + if (data.includes("fakeServer")) { + ok (false, "Should not try fetch"); + } + } + }, + remove() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} + +window.examiner = new examiner(); + +let div = document.getElementsByTagName("div")[0]; +div.innerHTML = "<svg><style><title><audio src=fakeServer onerror=fail() onload=fail()>"; + +let svg = div.firstChild; +is(svg.nodeName, "svg", "Node name should be svg"); + +let style = svg.firstChild; +if (style) { + is(style.firstChild, null, "Style should not have child nodes."); +} else { + ok(false, "Should have gotten a node."); +} + + +SimpleTest.executeSoon(function() { + window.examiner.remove(); + SimpleTest.finish(); +}); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_innerhtml_sanitizer.xhtml b/dom/security/test/general/test_innerhtml_sanitizer.xhtml new file mode 100644 index 0000000000..4d938bc23b --- /dev/null +++ b/dom/security/test/general/test_innerhtml_sanitizer.xhtml @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Bug 1667113</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1667113">Mozilla Bug 1667113</a> +<div></div> +<script><![CDATA[ +SimpleTest.waitForExplicitFinish(); + +// Please note that 'fakeServer' does not exist because the test relies +// on "csp-on-violate-policy" , and "specialpowers-http-notify-request" +// which fire if either the request is blocked or fires. The test does +// not rely on the result of the load. + +function fail() { + ok(false, "Should not call this") +} + +function examiner() { + SpecialPowers.addObserver(this, "csp-on-violate-policy"); + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(subject, topic, data) { + if (topic === "csp-on-violate-policy") { + let asciiSpec = SpecialPowers.getPrivilegedProps( + SpecialPowers.do_QueryInterface(subject, "nsIURI"), + "asciiSpec"); + if (asciiSpec.includes("fakeServer")) { + ok (false, "Should not attempt fetch, not even blocked by CSP."); + } + } + + if (topic === "specialpowers-http-notify-request") { + if (data.includes("fakeServer")) { + ok (false, "Should not try fetch"); + } + } + }, + remove() { + SpecialPowers.removeObserver(this, "csp-on-violate-policy"); + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} + +window.examiner = new examiner(); + +let div = document.getElementsByTagName("div")[0]; +div.innerHTML = "<svg xmlns='http://www.w3.org/2000/svg'><style><title><audio xmlns='http://www.w3.org/1999/xhtml' src='fakeServer' onerror='fail()' onload='fail()'></audio></title></style></svg>"; + +let svg = div.firstChild; +is(svg.nodeName, "svg", "Node name should be svg"); + +let style = svg.firstChild; +if (style) { + is(style.firstChild, null, "Style should not have child nodes."); +} else { + ok(false, "Should have gotten a node."); +} + + +SimpleTest.executeSoon(function() { + window.examiner.remove(); + SimpleTest.finish(); +}); + +]]></script> +</body> +</html> diff --git a/dom/security/test/general/test_meta_referrer.html b/dom/security/test/general/test_meta_referrer.html new file mode 100644 index 0000000000..f5e8b649f4 --- /dev/null +++ b/dom/security/test/general/test_meta_referrer.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1704473 - Remove head requirement for meta name=referrer</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<iframe id="frame_meta_in_head"></iframe> +<iframe id="frame_meta_notin_head"></iframe> + +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +let testCounter = 0; +function checkTestsDone() { + testCounter++; + if(testCounter == 2) { + SimpleTest.finish(); + } +} +var script = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + let counter = 0; + Services.obs.addObserver(function onExamResp(subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (!channel.URI.spec.startsWith("https://example.com") || counter >= 2) { + return; + } + + let refererHeaderSet = false; + try { + channel.getRequestHeader("referer"); + refererHeaderSet = true; + } catch (e) { + refererHeaderSet = false; + } + ok(!refererHeaderSet, "the referer header should not be set"); + counter++; + sendAsyncMessage("checked-referer-header"); + }, "http-on-stop-request"); +}); + +script.addMessageListener("checked-referer-header", checkTestsDone); + +let frame1 = document.getElementById("frame_meta_in_head"); +frame1.src = "/tests/dom/security/test/general/file_meta_referrer_in_head.html"; +let frame2 = document.getElementById("frame_meta_notin_head"); +frame2.src = "/tests/dom/security/test/general/file_meta_referrer_notin_head.html"; + +</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 0000000000..a22386aea0 --- /dev/null +++ b/dom/security/test/general/test_nosniff.html @@ -0,0 +1,88 @@ +<!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 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> + +<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 = 4; + +var testCounter = 0; +function checkFinish() { + testCounter++; + if (testCounter === NUM_TESTS) { + SimpleTest.finish(); + } +} + + // 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"; + + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_nosniff_navigation.html b/dom/security/test/general/test_nosniff_navigation.html new file mode 100644 index 0000000000..6710f4f5b9 --- /dev/null +++ b/dom/security/test/general/test_nosniff_navigation.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Bug 1428473 Support X-Content-Type-Options: nosniff when navigating</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + + <!-- add the two script tests --> + <script id="scriptCorrectType"></script> + <script id="scriptWrongType"></script> + + <script class="testbody" type="text/javascript"> + /* Description of the test: + * We're testing if Firefox respects the nosniff Header for Top-Level + * Navigations. + * If Firefox cant Display the Page, it will prompt a download + * and the URL of the Page will be about:blank. + * So we will try to open different content send with + * no-mime, mismatched-mime and garbage-mime types. + * + */ + + SimpleTest.waitForExplicitFinish(); + + window.addEventListener("load", async () => { + window.open("window_nosniff_navigation.html"); + }); + </script> +</body> +</html> diff --git a/dom/security/test/general/test_same_site_cookies_about.html b/dom/security/test/general/test_same_site_cookies_about.html new file mode 100644 index 0000000000..faf2caab9a --- /dev/null +++ b/dom/security/test/general/test_same_site_cookies_about.html @@ -0,0 +1,116 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1454721 - Add same-site cookie test for about:blank and about:srcdoc</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<img id="cookieImage"> +<iframe id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * 1) We load an image from http://mochi.test which sets a same site cookie + * 2) We then load the following iframes: + * (a) cross-origin iframe + * (b) same-origin iframe + * which both load a: + * * nested about:srcdoc frame and nested about:blank frame + * * navigate about:srcdoc frame and navigate about:blank frame + * 3) We evaluate that the same-site cookie is available in the same-origin case. + */ + +SimpleTest.waitForExplicitFinish(); + +const SAME_ORIGIN = "http://mochi.test:8888/" +const CROSS_ORIGIN = "http://example.com/"; +const PATH = "tests/dom/security/test/general/file_same_site_cookies_about.sjs"; + +let curTest = 0; + +var tests = [ + // NAVIGATION TESTS + { + description: "nested same origin iframe about:srcdoc navigation [mochi.test -> mochi.test -> about:srcdoc -> mochi.test]", + frameSRC: SAME_ORIGIN + PATH + "?loadsrcdocframeNav", + result: "myKey=mySameSiteAboutCookie", // cookie should be set for baseline test + }, + { + description: "nested cross origin iframe about:srcdoc navigation [mochi.test -> example.com -> about:srcdoc -> mochi.test]", + frameSRC: CROSS_ORIGIN + PATH + "?loadsrcdocframeNav", + result: "", // no same-site cookie should be available + }, + { + description: "nested same origin iframe about:blank navigation [mochi.test -> mochi.test -> about:blank -> mochi.test]", + frameSRC: SAME_ORIGIN + PATH + "?loadblankframeNav", + result: "myKey=mySameSiteAboutCookie", // cookie should be set for baseline test + }, + { + description: "nested cross origin iframe about:blank navigation [mochi.test -> example.com -> about:blank -> mochi.test]", + frameSRC: CROSS_ORIGIN + PATH + "?loadblankframeNav", + result: "", // no same-site cookie should be available + }, + // INCLUSION TESTS + { + description: "nested same origin iframe about:srcdoc inclusion [mochi.test -> mochi.test -> about:srcdoc -> mochi.test]", + frameSRC: SAME_ORIGIN + PATH + "?loadsrcdocframeInc", + result: "myKey=mySameSiteAboutCookie", // cookie should be set for baseline test + }, + { + description: "nested cross origin iframe about:srcdoc inclusion [mochi.test -> example.com -> about:srcdoc -> mochi.test]", + frameSRC: CROSS_ORIGIN + PATH + "?loadsrcdocframeInc", + result: "", // no same-site cookie should be available + }, + { + description: "nested same origin iframe about:blank inclusion [mochi.test -> mochi.test -> about:blank -> mochi.test]", + frameSRC: SAME_ORIGIN + PATH + "?loadblankframeInc", + result: "myKey=mySameSiteAboutCookie", // cookie should be set for baseline test + }, + { + description: "nested cross origin iframe about:blank inclusion [mochi.test -> example.com -> about:blank -> mochi.test]", + frameSRC: CROSS_ORIGIN + PATH + "?loadblankframeInc", + result: "", // no same-site cookie should be available + }, +]; + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + is(event.data.result, tests[curTest].result, tests[curTest].description); + curTest += 1; + + // lets see if we ran all the tests + if (curTest == tests.length) { + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); + return; + } + // otherwise it's time to run the next test + setCookieAndInitTest(); +} + +function setupQueryResultAndRunTest() { + let testframe = document.getElementById("testframe"); + testframe.src = tests[curTest].frameSRC + curTest; +} + +function setCookieAndInitTest() { + var cookieImage = document.getElementById("cookieImage"); + cookieImage.onload = function() { + ok(true, "trying to set cookie for test (" + tests[curTest].description + ")"); + setupQueryResultAndRunTest(); + } + cookieImage.onerror = function() { + ok(false, "could not load image for test (" + tests[curTest].description + ")"); + } + cookieImage.src = SAME_ORIGIN + PATH + "?setSameSiteCookie" + curTest; +} + +// fire up the test +setCookieAndInitTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_same_site_cookies_cross_origin_context.html b/dom/security/test/general/test_same_site_cookies_cross_origin_context.html new file mode 100644 index 0000000000..9294a3d030 --- /dev/null +++ b/dom/security/test/general/test_same_site_cookies_cross_origin_context.html @@ -0,0 +1,93 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1452496 - Do not allow same-site cookies in cross site context</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<img id="cookieImage"> +<iframe id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * 1) We load an image from http://example.com which tries to + * a) a same site cookie + * b) a regular cookie + * in the context of http://mochi.test + * 2) We load an iframe from http://example.com and check if the cookie + * is available. + * 3) We observe that: + * (a) same site cookie has been discarded in a cross origin context. + * (b) the regular cookie is available. + */ + +SimpleTest.waitForExplicitFinish(); + +const CROSS_ORIGIN = "http://example.com/"; +const PATH = "tests/dom/security/test/general/file_same_site_cookies_cross_origin_context.sjs"; + +let curTest = 0; + +var tests = [ + { + description: "regular cookie in cross origin context", + imgSRC: CROSS_ORIGIN + PATH + "?setRegularCookie", + frameSRC: CROSS_ORIGIN + PATH + "?loadFrame", + result: "myKey=regularCookie", + }, + { + description: "same-site cookie in cross origin context", + imgSRC: CROSS_ORIGIN + PATH + "?setSameSiteCookie", + frameSRC: CROSS_ORIGIN + PATH + "?loadFrame", + result: "", // no cookie should be set + }, +]; + + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + is(event.data.result, tests[curTest].result, tests[curTest].description); + curTest += 1; + + // lets see if we ran all the tests + if (curTest == tests.length) { + window.removeEventListener("message", receiveMessage); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + SimpleTest.finish(); + return; + } + // otherwise it's time to run the next test + setCookieAndInitTest(); +} + +function setupQueryResultAndRunTest() { + let testframe = document.getElementById("testframe"); + testframe.src = tests[curTest].frameSRC + curTest; +} + +function setCookieAndInitTest() { + var cookieImage = document.getElementById("cookieImage"); + cookieImage.onload = function() { + ok(true, "trying to set cookie for test (" + tests[curTest].description + ")"); + setupQueryResultAndRunTest(); + } + cookieImage.onerror = function() { + ok(false, "could not load image for test (" + tests[curTest].description + ")"); + } + cookieImage.src = tests[curTest].imgSRC + curTest; +} + +// fire up the test +SpecialPowers.pushPrefEnv({ + "set": [ + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ] +}, setCookieAndInitTest); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_same_site_cookies_from_script.html b/dom/security/test/general/test_same_site_cookies_from_script.html new file mode 100644 index 0000000000..74c38b6249 --- /dev/null +++ b/dom/security/test/general/test_same_site_cookies_from_script.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1452496 - Do not allow same-site cookies in cross site context</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<iframe id="setCookieFrame"></iframe> +<iframe id="getCookieFrame"></iframe> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * 1) We load an iframe which tries to set a same site cookie using an + * inline script in top-level context of http://mochi.test. + * 2) We load an iframe from http://example.com and check if the cookie + * is available. + * 3) We observe that: + * (a) same site cookie is available in same origin context. + * (a) same site cookie has been discarded in a cross origin context. + */ + +SimpleTest.waitForExplicitFinish(); + +const SAME_ORIGIN = "http://mochi.test:8888/"; +const CROSS_ORIGIN = "http://example.com/"; +const PATH = "tests/dom/security/test/general/file_same_site_cookies_from_script.sjs"; + +let curTest = 0; + +var tests = [ + { + description: "same-site cookie inline script within same-site context", + setCookieSrc: SAME_ORIGIN + PATH + "?setSameSiteCookieUsingInlineScript", + getCookieSrc: SAME_ORIGIN + PATH + "?getCookieFrame", + result: "myKey=sameSiteCookieInlineScript", + }, + { + description: "same-site cookie inline script within cross-site context", + setCookieSrc: CROSS_ORIGIN + PATH + "?setSameSiteCookieUsingInlineScript", + getCookieSrc: CROSS_ORIGIN + PATH + "?getCookieFrame", + result: "", // same-site cookie should be discarded in cross site context + }, +]; + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + is(event.data.result, tests[curTest].result, tests[curTest].description); + curTest += 1; + + // lets see if we ran all the tests + if (curTest == tests.length) { + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); + return; + } + // otherwise it's time to run the next test + setCookieAndInitTest(); +} + +function setupQueryResultAndRunTest() { + let getCookieFrame = document.getElementById("getCookieFrame"); + getCookieFrame.src = tests[curTest].getCookieSrc + curTest; +} + +function setCookieAndInitTest() { + var setCookieFrame = document.getElementById("setCookieFrame"); + setCookieFrame.onload = function() { + ok(true, "trying to set cookie for test (" + tests[curTest].description + ")"); + setupQueryResultAndRunTest(); + } + setCookieFrame.onerror = function() { + ok(false, "could not load image for test (" + tests[curTest].description + ")"); + } + setCookieFrame.src = tests[curTest].setCookieSrc + curTest; +} + +// fire up the test +setCookieAndInitTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_same_site_cookies_iframe.html b/dom/security/test/general/test_same_site_cookies_iframe.html new file mode 100644 index 0000000000..45d5d5830a --- /dev/null +++ b/dom/security/test/general/test_same_site_cookies_iframe.html @@ -0,0 +1,168 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1454027 - Update SameSite cookie handling inside iframes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<img id="cookieImage"> +<iframe id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * 1) We load an image from http://mochi.test which sets a same site cookie + * 2) We then load the following iframes: + * (a) cross-origin iframe + * (b) sandboxed iframe + * (c) data: URI iframe + * (d) same origin iframe which loads blob: URI iframe (to simulate same origin blobs) + * (e) cross origin iframe which loads blob: URI iframe (to simulate cross origin blobs) + * which all: + * * navigate the iframe to http://mochi.test + * * include another iframe from http://mochi.test + * 3) We observe that none of the nested iframes have access to the same-site cookie. + */ + +SimpleTest.waitForExplicitFinish(); + +const SAME_ORIGIN = "http://mochi.test:8888/" +const CROSS_ORIGIN = "http://example.com/"; +const PATH = "tests/dom/security/test/general/"; +const SERVER_FILE = "file_same_site_cookies_iframe.sjs"; + +const NESTED_DATA_IFRAME_NAVIGATION = ` + data:text/html, + <html> + <body> + <a id="testlink" href="http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_iframe.sjs"></a> + <script type="application/javascript"> + let link = document.getElementById("testlink"); + link.click(); + <\/script> + </body> + </html>`; + +const NESTED_DATA_IFRAME_INCLUSION = ` + data:text/html, + <html> + <body> + <script type="application/javascript"> + window.addEventListener("message", receiveMessage); + function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + window.parent.postMessage({result: event.data.result}, '*'); + } + <\/script> + <iframe src="http://mochi.test:8888/tests/dom/security/test/general/file_same_site_cookies_iframe.sjs"></iframe> + </body> + </html>`; + +let curTest = 0; + +var tests = [ + // NAVIGATION TESTS + { + description: "nested same origin iframe navigation [mochi.test -> mochi.test -> mochi.test]", + frameSRC: SAME_ORIGIN + PATH + SERVER_FILE + "?nestedIframeNavigation", + result: "myKey=mySameSiteIframeTestCookie", // cookie should be set for baseline test + }, + { + description: "nested cross origin iframe navigation [mochi.test -> example.com -> mochi.test]", + frameSRC: CROSS_ORIGIN + PATH + SERVER_FILE + "?nestedIframeNavigation", + result: "", // no cookie should be set + }, + { + description: "nested sandboxed iframe navigation [mochi.test -> sandbox -> mochi.test]", + frameSRC: CROSS_ORIGIN + PATH + SERVER_FILE + "?nestedSandboxIframeNavigation", + result: "", // no cookie should be set + }, + { + description: "nested data iframe navigation [mochi.test -> data: -> mochi.test]", + frameSRC: NESTED_DATA_IFRAME_NAVIGATION, + result: "", // no cookie should be set + }, + { + description: "nested same site blob iframe navigation [mochi.test -> mochi.test -> blob: -> mochi.test]", + frameSRC: SAME_ORIGIN + PATH + "file_same_site_cookies_blob_iframe_navigation.html", + result: "myKey=mySameSiteIframeTestCookie", // cookie should be set, blobs inherit security context + }, + { + description: "nested cross site blob iframe navigation [mochi.test -> example.com -> blob: -> mochi.test]", + frameSRC: CROSS_ORIGIN + PATH + "file_same_site_cookies_blob_iframe_navigation.html", + result: "", // no cookie should be set + }, + // INCLUSION TESTS + { + description: "nested same origin iframe inclusion [mochi.test -> mochi.test -> mochi.test]", + frameSRC: SAME_ORIGIN + PATH + SERVER_FILE + "?nestedIframeInclusion", + result: "myKey=mySameSiteIframeTestCookie", // cookie should be set for baseline test + }, + { + description: "nested cross origin iframe inclusion [mochi.test -> example.com -> mochi.test]", + frameSRC: CROSS_ORIGIN + PATH + SERVER_FILE + "?nestedIframeInclusion", + result: "", // no cookie should be set + }, + { + description: "nested sandboxed iframe inclusion [mochi.test -> sandbox -> mochi.test]", + frameSRC: CROSS_ORIGIN + PATH + SERVER_FILE + "?nestedSandboxIframeInclusion", + result: "", // no cookie should be set + }, + { + description: "nested data iframe inclusion [mochi.test -> data: -> mochi.test]", + frameSRC: NESTED_DATA_IFRAME_INCLUSION, + result: "", // no cookie should be set + }, + { + description: "nested same site blob iframe inclusion [mochi.test -> mochi.test -> blob: -> mochi.test]", + frameSRC: SAME_ORIGIN + PATH + "file_same_site_cookies_blob_iframe_inclusion.html", + result: "myKey=mySameSiteIframeTestCookie", // cookie should be set, blobs inherit security context + }, + { + description: "same-site cookie, nested cross site blob iframe inclusion [mochi.test -> example.com -> blob: -> mochi.test]", + frameSRC: CROSS_ORIGIN + PATH + "file_same_site_cookies_blob_iframe_inclusion.html", + result: "", // no cookie should be set + }, +]; + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + is(event.data.result, tests[curTest].result, tests[curTest].description); + curTest += 1; + + // // lets see if we ran all the tests + if (curTest == tests.length) { + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); + return; + } + // otherwise it's time to run the next test + setCookieAndInitTest(); +} + +function setupQueryResultAndRunTest() { + let testframe = document.getElementById("testframe"); + testframe.src = tests[curTest].frameSRC; +} + +function setCookieAndInitTest() { + var cookieImage = document.getElementById("cookieImage"); + cookieImage.onload = function() { + ok(true, "trying to set cookie for test (" + tests[curTest].description + ")"); + setupQueryResultAndRunTest(); + } + cookieImage.onerror = function() { + ok(false, "could not load image for test (" + tests[curTest].description + ")"); + } + // appending math.random to avoid any unexpected caching behavior + cookieImage.src = SAME_ORIGIN + PATH + SERVER_FILE + "?setSameSiteCookie" + Math.random(); +} + +// fire up the test +setCookieAndInitTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_same_site_cookies_laxByDefault.html b/dom/security/test/general/test_same_site_cookies_laxByDefault.html new file mode 100644 index 0000000000..9fd0d0b704 --- /dev/null +++ b/dom/security/test/general/test_same_site_cookies_laxByDefault.html @@ -0,0 +1,85 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1551798 - SameSite=lax by default</title> + <script 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 CROSS_ORIGIN = "http://example.com/"; +const PATH = "tests/dom/security/test/general/closeWindow.sjs"; + +async function realTest(noneRequiresSecure) { + let types = ["unset", "lax", "none"]; + for (let i = 0; i < types.length; ++i) { + info("Loading a new top-level page (" + types[i] + ")"); + await new Promise(resolve => { + window.addEventListener("message", _ => { + resolve(); + }, { once: true }); + window.open(CROSS_ORIGIN + PATH + "?" + types[i]); + }); + } + + info("Check cookies"); + let chromeScript = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + const {sendAsyncMessage} = this; + let cookies = { test: null, test2: null, test3: null }; + + for (let cookie of Services.cookies.cookies) { + if (cookie.host != "example.com") continue; + + if (cookie.name == "test" && cookie.value == "wow") { + cookies.test = cookie.sameSite == Ci.nsICookie.SAMESITE_LAX ? 'lax' : 'none'; + } + + if (cookie.name == "test2" && cookie.value == "wow2") { + cookies.test2 = cookie.sameSite == Ci.nsICookie.SAMESITE_LAX ? 'lax' : 'none'; + } + + if (cookie.name == "test3" && cookie.value == "wow3") { + cookies.test3 = cookie.sameSite == Ci.nsICookie.SAMESITE_LAX ? 'lax' : 'none'; + } + } + + Services.cookies.removeAll(); + sendAsyncMessage('result', cookies); + }); + + let cookies = await new Promise(resolve => { + chromeScript.addMessageListener('result', cookies => { + chromeScript.destroy(); + resolve(cookies); + }); + }); + + is(cookies.test, "lax", "Cookie set without samesite is lax by default"); + if (noneRequiresSecure) { + is(cookies.test2, null, "Cookie set with samesite none, but not secure"); + } else { + is(cookies.test2, "none", "Cookie set with samesite none"); + } + is(cookies.test3, "lax", "Cookie set with samesite lax"); +} + +SpecialPowers.pushPrefEnv({"set": [ + ["network.cookie.sameSite.laxByDefault", true], + ["network.cookie.sameSite.noneRequiresSecure", false], +]}).then(_ => { + return realTest(false); +}).then(_ => { + return SpecialPowers.pushPrefEnv({"set": [ + ["network.cookie.sameSite.laxByDefault", true], + ["network.cookie.sameSite.noneRequiresSecure", true]]}); +}).then(_ => { + return realTest(true); +}).then(SimpleTest.finish); + +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_same_site_cookies_redirect.html b/dom/security/test/general/test_same_site_cookies_redirect.html new file mode 100644 index 0000000000..59f98b2263 --- /dev/null +++ b/dom/security/test/general/test_same_site_cookies_redirect.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1453814 - Do not allow same-site cookies for cross origin redirect</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<img id="cookieImage"> +<iframe id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * 1) We load an image from http://mochi.test which set a same site cookie + * 2) We then load an iframe that redirects + * (a) from same-origin to cross-origin + * (b) from cross-origin to same-origin + * 3) We observe that in both cases same-site cookies should not be send + */ + +SimpleTest.waitForExplicitFinish(); + +const SAME_ORIGIN = location.origin + "/"; +const CROSS_ORIGIN = "http://example.com/"; +const PATH = "tests/dom/security/test/general/file_same_site_cookies_redirect.sjs"; + +let curTest = 0; + +var tests = [ + { + description: "baseline: same-site cookie, redirect same-site to same-site", + imgSRC: SAME_ORIGIN + PATH + "?setSameSiteCookie", + frameSRC: SAME_ORIGIN + PATH + "?sameToSameRedirect", + result: "myKey=strictSameSiteCookie", + }, + { + description: "same-site cookie, redirect same-site to cross-site", + imgSRC: SAME_ORIGIN + PATH + "?setSameSiteCookie", + frameSRC: SAME_ORIGIN + PATH + "?sameToCrossRedirect", + result: "", // no cookie should be set + }, + { + description: "same-site cookie, redirect cross-site to same-site", + imgSRC: SAME_ORIGIN + PATH + "?setSameSiteCookie", + frameSRC: CROSS_ORIGIN + PATH + "?crossToSameRedirect", + result: "", // no cookie should be set + }, + { + description: "same-site cookie, meta redirect same-site to cross-site", + imgSRC: SAME_ORIGIN + PATH + "?setSameSiteCookie", + frameSRC: SAME_ORIGIN + PATH + "?sameToCrossRedirectMeta", + result: "", // no cookie should be set + }, + { + description: "same-site cookie, meta redirect cross-site to same-site", + imgSRC: SAME_ORIGIN + PATH + "?setSameSiteCookie", + frameSRC: CROSS_ORIGIN + PATH + "?crossToSameRedirectMeta", + result: "", // no cookie should be set + }, +]; + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + is(event.data.result, tests[curTest].result, tests[curTest].description); + curTest += 1; + + // // lets see if we ran all the tests + if (curTest == tests.length) { + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); + return; + } + // otherwise it's time to run the next test + setCookieAndInitTest(); +} + +function setupQueryResultAndRunTest() { + let testframe = document.getElementById("testframe"); + testframe.src = tests[curTest].frameSRC; +} + +function setCookieAndInitTest() { + var cookieImage = document.getElementById("cookieImage"); + cookieImage.onload = function() { + ok(true, "trying to set cookie for test (" + tests[curTest].description + ")"); + setupQueryResultAndRunTest(); + } + cookieImage.onerror = function() { + ok(false, "could not load image for test (" + tests[curTest].description + ")"); + } + cookieImage.src = tests[curTest].imgSRC; +} + +// fire up the test +setCookieAndInitTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_same_site_cookies_subrequest.html b/dom/security/test/general/test_same_site_cookies_subrequest.html new file mode 100644 index 0000000000..304dbafa9a --- /dev/null +++ b/dom/security/test/general/test_same_site_cookies_subrequest.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1286861 - Test same site cookies on subrequests</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<img id="cookieImage"> +<iframe id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * 1) We load an image from http://mochi.test which sets a same site cookie + * 2) We load an iframe from: + * * http://mochi.test which loads another image from http://mochi.test + * * http://example.com which loads another image from http://mochi.test + * 3) We observe that the same site cookie is sent in the same origin case, + * but not in the cross origin case. + * + * In detail: + * We perform an XHR request to the *.sjs file which is processed async on + * the server and waits till the image request has been processed by the server. + * Once the image requets was processed, the server responds to the initial + * XHR request with the expecuted result (the cookie value). + */ + +SimpleTest.waitForExplicitFinish(); + +const SAME_ORIGIN = "http://mochi.test:8888/"; +const CROSS_ORIGIN = "http://example.com/"; +const PATH = "tests/dom/security/test/general/file_same_site_cookies_subrequest.sjs"; + +let curTest = 0; + +var tests = [ + { + description: "same origin site using cookie policy 'samesite=strict'", + imgSRC: SAME_ORIGIN + PATH + "?setStrictSameSiteCookie", + frameSRC: SAME_ORIGIN + PATH + "?loadFrame", + result: "myKey=strictSameSiteCookie", + }, + { + description: "cross origin site using cookie policy 'samesite=strict'", + imgSRC: SAME_ORIGIN + PATH + "?setStrictSameSiteCookie", + frameSRC: CROSS_ORIGIN + PATH + "?loadFrame", + result: "myKey=noCookie", + }, + { + description: "same origin site using cookie policy 'samesite=lax'", + imgSRC: SAME_ORIGIN + PATH + "?setLaxSameSiteCookie", + frameSRC: SAME_ORIGIN + PATH + "?loadFrame", + result: "myKey=laxSameSiteCookie", + }, + { + description: "cross origin site using cookie policy 'samesite=lax'", + imgSRC: SAME_ORIGIN + PATH + "?setLaxSameSiteCookie", + frameSRC: CROSS_ORIGIN + PATH + "?loadFrame", + result: "myKey=noCookie", + }, +]; + +function checkResult(aCookieVal) { + is(aCookieVal, tests[curTest].result, tests[curTest].description); + curTest += 1; + + // lets see if we ran all the tests + if (curTest == tests.length) { + SimpleTest.finish(); + return; + } + // otherwise it's time to run the next test + setCookieAndInitTest(); +} + +function setupQueryResultAndRunTest() { + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "file_same_site_cookies_subrequest.sjs?queryresult" + curTest); + myXHR.onload = function(e) { + checkResult(myXHR.responseText); + } + myXHR.onerror = function(e) { + ok(false, "could not query results from server (" + e.message + ")"); + } + myXHR.send(); + + // give it some time and load the test frame + SimpleTest.executeSoon(function() { + let testframe = document.getElementById("testframe"); + testframe.src = tests[curTest].frameSRC + curTest; + }); +} + +function setCookieAndInitTest() { + var cookieImage = document.getElementById("cookieImage"); + cookieImage.onload = function() { + ok(true, "set cookie for test (" + tests[curTest].description + ")"); + setupQueryResultAndRunTest(); + } + cookieImage.onerror = function() { + ok(false, "could not set cookie for test (" + tests[curTest].description + ")"); + } + cookieImage.src = tests[curTest].imgSRC + curTest; +} + +// fire up the test +setCookieAndInitTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_same_site_cookies_toplevel_nav.html b/dom/security/test/general/test_same_site_cookies_toplevel_nav.html new file mode 100644 index 0000000000..aba825916b --- /dev/null +++ b/dom/security/test/general/test_same_site_cookies_toplevel_nav.html @@ -0,0 +1,117 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1286861 - Test same site cookies on top-level navigations</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<img id="cookieImage"> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * 1) We load an image from http://mochi.test which sets a same site cookie + * 2) We open a new window to + * * a same origin location + * * a cross origin location + * 3) We observe that the same site cookie is sent in the same origin case, + * but not in the cross origin case, unless the policy = 'lax', which should + * send the cookie in a top-level navigation case. + * + * In detail: + * We perform an XHR request to the *.sjs file which is processed async on + * the server and waits till the image request has been processed by the server. + * Once the image requets was processed, the server responds to the initial + * XHR request with the expecuted result (the cookie value). + */ + +SimpleTest.waitForExplicitFinish(); + +const SAME_ORIGIN = "http://mochi.test:8888/"; +const CROSS_ORIGIN = "http://example.com/"; +const PATH = "tests/dom/security/test/general/file_same_site_cookies_toplevel_nav.sjs"; + +let curTest = 0; + +let currentWindow; +var tests = [ + { + description: "same origin navigation using cookie policy 'samesite=strict'", + imgSRC: SAME_ORIGIN + PATH + "?setStrictSameSiteCookie", + frameSRC: SAME_ORIGIN + PATH + "?loadFrame", + result: "myKey=strictSameSiteCookie", + }, + { + description: "cross origin navigation using cookie policy 'samesite=strict'", + imgSRC: SAME_ORIGIN + PATH + "?setStrictSameSiteCookie", + frameSRC: CROSS_ORIGIN + PATH + "?loadFrame", + result: "myKey=noCookie", + }, + { + description: "same origin navigation using cookie policy 'samesite=lax'", + imgSRC: SAME_ORIGIN + PATH + "?setLaxSameSiteCookie", + frameSRC: SAME_ORIGIN + PATH + "?loadFrame", + result: "myKey=laxSameSiteCookie", + }, + { + description: "cross origin navigation using cookie policy 'samesite=lax'", + imgSRC: SAME_ORIGIN + PATH + "?setLaxSameSiteCookie", + frameSRC: CROSS_ORIGIN + PATH + "?loadFrame", + result: "myKey=laxSameSiteCookie", + }, +]; + +function checkResult(aCookieVal) { + if(currentWindow){ + currentWindow.close(); + currentWindow= null; + } + is(aCookieVal, tests[curTest].result, tests[curTest].description); + curTest += 1; + + // lets see if we ran all the tests + if (curTest == tests.length) { + SimpleTest.finish(); + return; + } + // otherwise it's time to run the next test + setCookieAndInitTest(); +} + +function setupQueryResultAndRunTest() { + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "file_same_site_cookies_toplevel_nav.sjs?queryresult" + curTest); + myXHR.onload = function(e) { + checkResult( myXHR.responseText); + } + myXHR.onerror = function(e) { + ok(false, "could not query results from server (" + e.message + ")"); + } + myXHR.send(); + + // give it some time and load the test window + SimpleTest.executeSoon(function() { + currentWindow = window.open(tests[curTest].frameSRC + curTest); + }); +} + +function setCookieAndInitTest() { + var cookieImage = document.getElementById("cookieImage"); + cookieImage.onload = function() { + ok(true, "set cookie for test (" + tests[curTest].description + ")"); + setupQueryResultAndRunTest(); + } + cookieImage.onerror = function() { + ok(false, "could not set cookie for test (" + tests[curTest].description + ")"); + } + cookieImage.src = tests[curTest].imgSRC + curTest; +} + +// fire up the test +setCookieAndInitTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_same_site_cookies_toplevel_set_cookie.html b/dom/security/test/general/test_same_site_cookies_toplevel_set_cookie.html new file mode 100644 index 0000000000..cae2a6174e --- /dev/null +++ b/dom/security/test/general/test_same_site_cookies_toplevel_set_cookie.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1454242: Setting samesite cookie should not rely on CookieCommons::IsSameSiteForeign</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<img id="cookieImage"> +<iframe id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * 1) We load a window from example.com which loads a window from mochi.test + * which then sets a same-site cookie for mochi.test. + * 2) We load an iframe from mochi.test. + * 3) We observe that the cookie within (1) was allowed to be set and + * is available for mochi.test. + */ + +SimpleTest.waitForExplicitFinish(); + +const SAME_ORIGIN = "http://mochi.test:8888/" +const CROSS_ORIGIN = "http://example.com/"; +const PATH = "tests/dom/security/test/general/file_same_site_cookies_toplevel_set_cookie.sjs"; + +let testWin = null; + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + // once the second window (which sets the cookie) loaded, we get a notification + // that the test setup is correct and we can now try to query the same-site cookie + if (event.data.value === "testSetupComplete") { + ok(true, "cookie setup worked"); + let testframe = document.getElementById("testframe"); + testframe.src = SAME_ORIGIN + PATH + "?checkCookie"; + return; + } + + // thie second message is the cookie value from verifying the + // cookie has been set correctly. + is(event.data.value, "myKey=laxSameSiteCookie", + "setting same-site cookie on cross origin top-level page"); + + window.removeEventListener("message", receiveMessage); + testWin.close(); + SimpleTest.finish(); +} + +// fire up the test +testWin = window.open(CROSS_ORIGIN + PATH + "?loadWin"); + +</script> +</body> +</html> diff --git a/dom/security/test/general/test_xfo_error_page.html b/dom/security/test/general/test_xfo_error_page.html new file mode 100644 index 0000000000..218413b4f9 --- /dev/null +++ b/dom/security/test/general/test_xfo_error_page.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1626249: Ensure correct display of neterror page for XFO</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe style="width:100%;" id="xfo_testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +const XFO_ERROR_PAGE_MSG = "This page has an X-Frame-Options policy that prevents it from being loaded in this context"; + +let xfo_testframe = document.getElementById("xfo_testframe"); + +xfo_testframe.onload = function() { + let wrappedXFOFrame = SpecialPowers.wrap(xfo_testframe.contentWindow); + let frameContentXFO = wrappedXFOFrame.document.body.innerHTML; + ok(frameContentXFO.includes(XFO_ERROR_PAGE_MSG), "xfo error page correct"); + SimpleTest.finish(); +} + +xfo_testframe.onerror = function() { + ok(false, "sanity: should not fire onerror for xfo_testframe"); + SimpleTest.finish(); +} + +xfo_testframe.src = "file_xfo_error_page.sjs"; + +</script> +</body> +</html> diff --git a/dom/security/test/general/window_nosniff_navigation.html b/dom/security/test/general/window_nosniff_navigation.html new file mode 100644 index 0000000000..1287e451b1 --- /dev/null +++ b/dom/security/test/general/window_nosniff_navigation.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1428473 Support X-Content-Type-Options: nosniff when navigating</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <style> + iframe{ + border: 1px solid orange; + } + </style> + + <!-- Using Content-Type: */* --> + <iframe class="no-mime" src="file_nosniff_navigation.sjs?mime=*%2F*&content=xml"></iframe> + <iframe class="no-mime" src="file_nosniff_navigation.sjs?mime=*%2F*&content=html"></iframe> + <iframe class="no-mime" src="file_nosniff_navigation.sjs?mime=*%2F*&content=css" ></iframe> + <iframe class="no-mime" src="file_nosniff_navigation.sjs?mime=*%2F*&content=json"></iframe> + <iframe class="no-mime" src="file_nosniff_navigation.sjs?mime=*%2F*&content=img"></iframe> + <iframe class="no-mime" src="file_nosniff_navigation.sjs?mime=*%2F*&content=pdf"></iframe> + <iframe class="no-mime" src="file_nosniff_navigation.sjs?mime=*%2F*"></iframe> + <hr> + <!-- Using Content-Type: image/png --> + <iframe class="mismatch-mime" src="file_nosniff_navigation.sjs?mime=image%2Fpng&content=xml"></iframe> + <iframe class="mismatch-mime" src="file_nosniff_navigation.sjs?mime=image%2Fpng&content=html"></iframe> + <iframe class="mismatch-mime" src="file_nosniff_navigation.sjs?mime=image%2Fpng&content=css"></iframe> + <iframe class="mismatch-mime" src="file_nosniff_navigation.sjs?mime=image%2Fpng&content=json"></iframe> + <iframe class="mismatch-mime" src="file_nosniff_navigation.sjs?mime=image%2Fpng&content=img"></iframe> + <iframe class="mismatch-mime" src="file_nosniff_navigation.sjs?mime=image%2Fpng&content=pdf"></iframe> + <iframe class="mismatch-mime" src="file_nosniff_navigation.sjs?mime=image%2Fpng"></iframe> + <hr> + <!-- Using Content-Type: garbage/garbage --> + <iframe class="garbage-mime" src="file_nosniff_navigation.sjs?mime=garbage%2Fgarbage&content=xml"> </iframe> + <iframe class="garbage-mime" src="file_nosniff_navigation.sjs?mime=garbage%2Fgarbage&content=html"></iframe> + <iframe class="garbage-mime" src="file_nosniff_navigation.sjs?mime=garbage%2Fgarbage&content=css" ></iframe> + <iframe class="garbage-mime" src="file_nosniff_navigation.sjs?mime=garbage%2Fgarbage&content=json"></iframe> + <iframe class="garbage-mime" src="file_nosniff_navigation.sjs?mime=garbage%2Fgarbage&content=img"></iframe> + <iframe class="garbage-mime" src="file_nosniff_navigation.sjs?mime=garbage%2Fgarbage&content=pdf"></iframe> + <iframe class="garbage-mime" src="file_nosniff_navigation.sjs?mime=garbage%2Fgarbage"></iframe> +</head> + +<body> + +<!-- add the two script tests --> +<script id="scriptCorrectType"></script> +<script id="scriptWrongType"></script> + +<script class="testbody" type="text/javascript"> +/* Description of the test: + * We're testing if Firefox respects the nosniff Header for Top-Level + * Navigations. + * If Firefox cant Display the Page, it will prompt a download + * and the URL of the Page will be about:blank. + * So we will try to open different content send with + * no-mime, mismatched-mime and garbage-mime types. + * + */ + +SimpleTest.waitForExplicitFinish(); + +window.addEventListener("load", ()=>{ + let noMimeFrames = Array.from(document.querySelectorAll(".no-mime")); + noMimeFrames.forEach(frame => { + let doc = frame.contentWindow.document; + // In case of no Provided Content Type, not rendering or assuming text/plain is valid + let result = doc.URL == "about:blank" || doc.contentType == "text/plain"; + let sniffTarget = (new URL(frame.src)).searchParams.get("content"); + window.opener.ok(result, `${sniffTarget} without MIME - was not sniffed`); + }); + + let mismatchedMimes = Array.from(document.querySelectorAll(".mismatch-mime")); + mismatchedMimes.forEach(frame => { + // In case the Server mismatches the Mime Type (sends content X as image/png) + // assert that we do not sniff and correct this. + let result = frame.contentWindow.document.contentType == "image/png"; + let sniffTarget = (new URL(frame.src)).searchParams.get("content"); + window.opener.ok(result, `${sniffTarget} send as image/png - was not Sniffed`); + }); + + let badMimeFrames = Array.from(document.querySelectorAll(".garbage-mime")); + badMimeFrames.forEach(frame => { + // In the case we got a bogous mime, assert that we dont sniff. + // We must not default here to text/plain + // as the Server at least provided a mime type. + let result = frame.contentWindow.document.URL == "about:blank"; + let sniffTarget = (new URL(frame.src)).searchParams.get("content"); + window.opener.ok(result, `${sniffTarget} send as garbage/garbage - was not Sniffed`); + }); + + window.opener.SimpleTest.finish(); + this.close(); +}); +</script> +</body> + +</html>
\ No newline at end of file diff --git a/dom/security/test/gtest/TestCSPParser.cpp b/dom/security/test/gtest/TestCSPParser.cpp new file mode 100644 index 0000000000..735a6c7502 --- /dev/null +++ b/dom/security/test/gtest/TestCSPParser.cpp @@ -0,0 +1,1155 @@ +/* -*- 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> + +#include "nsIContentSecurityPolicy.h" +#include "nsNetUtil.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/nsCSPContext.h" +#include "mozilla/gtest/MozAssertions.h" +#include "nsComponentManagerUtils.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsStringFwd.h" + +/* + * 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 SetRequestContextWithPrincipal + * 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; + + // 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; + mozilla::OriginAttributes attrs; + selfURIPrincipal = + mozilla::BasePrincipal::CreateContentPrincipal(selfURI, attrs); + NS_ENSURE_TRUE(selfURIPrincipal, NS_ERROR_FAILURE); + + // 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->SetRequestContextWithPrincipal(selfURIPrincipal, selfURI, u""_ns, 0); + 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 navigateTo = false; + bool wasmUnsafeEval = false; + if (prefs) { + prefs->GetBoolPref("security.csp.enableNavigateTo", &navigateTo); + prefs->SetBoolPref("security.csp.enableNavigateTo", true); + prefs->GetBoolPref("security.csp.wasm-unsafe-eval.enabled", + &wasmUnsafeEval); + prefs->SetBoolPref("security.csp.wasm-unsafe-eval.enabled", 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.enableNavigateTo", navigateTo); + prefs->SetBoolPref("security.csp.wasm-unsafe-eval.enabled", wasmUnsafeEval); + } + + return NS_OK; +} + +// ============================= TestDirectives ======================== + +TEST(CSPParser, Directives) +{ + static const PolicyTest policies[] = { + // clang-format off + { "connect-src xn--mnchen-3ya.de", + "connect-src http://xn--mnchen-3ya.de"}, + { "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 'nonce-a'", + "script-src 'nonce-a'" }, + { "script-src 'sha256-a'", + "script-src 'sha256-a'" }, + { "script-src 'sha256-siVR8vAcqP06h2ppeNwqgjr0yZ6yned4X2VF84j4GmI='", + "script-src 'sha256-siVR8vAcqP06h2ppeNwqgjr0yZ6yned4X2VF84j4GmI='" }, + { "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:" }, + { "script-src 'nonce-foo' 'strict-dynamic' 'unsafe-inline' 'report-sample' https: ", + "script-src 'nonce-foo' 'strict-dynamic' 'unsafe-inline' 'report-sample' https:" }, + { "default-src 'sha256-siVR8' 'strict-dynamic' 'unsafe-inline' https: ", + "default-src 'sha256-siVR8' 'unsafe-inline' https:" }, + { "worker-src https://example.com", + "worker-src https://example.com" }, + { "worker-src http://worker.com; frame-src http://frame.com; child-src http://child.com", + "worker-src http://worker.com; frame-src http://frame.com; child-src http://child.com" }, + { "navigate-to http://example.com", + "navigate-to http://example.com"}, + { "navigate-to 'unsafe-allow-redirects' http://example.com", + "navigate-to 'unsafe-allow-redirects' http://example.com"}, + { "script-src 'unsafe-allow-redirects' http://example.com", + "script-src http://example.com"}, + // clang-format on + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1)); +} + +// ============================= TestKeywords ======================== + +TEST(CSPParser, Keywords) +{ + static const PolicyTest policies[] = { + // clang-format off + { "script-src 'self'", + "script-src 'self'" }, + { "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'" }, + { "script-src 'wasm-unsafe-eval'", + "script-src 'wasm-unsafe-eval'" }, + { "img-src 'none'; script-src 'unsafe-eval' 'unsafe-inline'; default-src 'self'", + "img-src 'none'; script-src 'unsafe-eval' 'unsafe-inline'; default-src 'self'" }, + // clang-format on + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1)); +} + +// =================== TestIgnoreUpperLowerCasePolicies ============== + +TEST(CSPParser, IgnoreUpperLowerCasePolicies) +{ + static const PolicyTest policies[] = { + // clang-format off + { "script-src 'SELF'", + "script-src 'self'" }, + { "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 'self'" }, + { "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='" }, + { "upgrade-INSECURE-requests", + "upgrade-insecure-requests" }, + { "sanDBox alloW-foRMs", + "sandbox allow-forms"}, + // clang-format on + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1)); +} + +// ========================= TestPaths =============================== + +TEST(CSPParser, Paths) +{ + static const PolicyTest policies[] = { + // clang-format off + { "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_" } + // clang-format on + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1)); +} + +// ======================== TestSimplePolicies ======================= + +TEST(CSPParser, SimplePolicies) +{ + static const PolicyTest policies[] = { + // clang-format off + { "frame-src intent:", + "frame-src intent:" }, + { "frame-src intent://host.name", + "frame-src intent://host.name" }, + { "frame-src intent://my.host.link/", + "frame-src intent://my.host.link/" }, + { "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 'self'" }, + { "style-src http://www.example.com 'self'", + "style-src http://www.example.com 'self'" }, + { "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 'self'; 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 'self'; 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 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" }, + // clang-format on + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1)); +} + +// =================== TestPoliciesWithInvalidSrc ==================== + +TEST(CSPParser, PoliciesWithInvalidSrc) +{ + static const PolicyTest policies[] = { + // clang-format off + { "script-src 'self'; SCRIPT-SRC http://www.example.com", + "script-src 'self'" }, + { "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'" }, + { "script-src 'nonce-{invalid}'", + "script-src 'none'" }, + { "script-src 'sha256-{invalid}'", + "script-src 'none'" }, + { "script-src 'nonce-in$valid'", + "script-src 'none'" }, + { "script-src 'sha256-in$valid'", + "script-src 'none'" }, + { "script-src 'nonce-invalid==='", + "script-src 'none'" }, + { "script-src 'sha256-invalid==='", + "script-src 'none'" }, + { "script-src 'nonce-==='", + "script-src 'none'" }, + { "script-src 'sha256-==='", + "script-src 'none'" }, + { "script-src 'nonce-=='", + "script-src 'none'" }, + { "script-src 'sha256-=='", + "script-src 'none'" }, + { "script-src 'nonce-='", + "script-src 'none'" }, + { "script-src 'sha256-='", + "script-src 'none'" }, + { "script-src 'nonce-'", + "script-src 'none'" }, + { "script-src 'sha256-'", + "script-src 'none'" }, + { "connect-src http://www.example.com/foo%zz;", + "connect-src 'none'" }, + { "script-src https://foo.com/%$", + "script-src 'none'" }, + { "sandbox foo", + "sandbox"}, + // clang-format on + }; + + // amount of tests - 1, because the latest should be ignored. + uint32_t policyCount = (sizeof(policies) / sizeof(PolicyTest)) - 1; + ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1)); +} + +// ============================= TestBadPolicies ======================= + +TEST(CSPParser, BadPolicies) +{ + static const PolicyTest policies[] = { + // clang-format off + { "script-sr 'self", "" }, + { "", "" }, + { "; ; ; ; ; ; ;", "" }, + { "defaut-src asdf", "" }, + { "default-src: aaa", "" }, + { "asdf http://test.com", ""}, + { "report-uri", ""}, + { "report-uri http://:foo", ""}, + { "require-sri-for", ""}, + { "require-sri-for style", ""}, + // clang-format on + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 0)); +} + +// ======================= TestGoodGeneratedPolicies ================= + +TEST(CSPParser, GoodGeneratedPolicies) +{ + static const PolicyTest policies[] = { + // clang-format off + { "default-src 'self'; img-src *", + "default-src 'self'; 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" }, + { "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:" }, + { "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" }, + { "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 'self'; script-src 'self' 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 'self'" }, + { "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 'self'" }, + { "DEFAULT-src 'self';", + "default-src 'self'" }, + { "default-src 'self' http://FOO.COM", + "default-src 'self' http://foo.com" }, + { "default-src 'self' HTTP://foo.com", + "default-src 'self' http://foo.com" }, + { "default-src 'NONE'", + "default-src 'none'" }, + { "script-src policy-uri ", + "script-src http://policy-uri" }, + { "img-src 'self'; ", + "img-src 'self'" }, + { "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 'self'" }, + { "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" }, + { "frame-ancestors http://sampleuser:samplepass@example.com", + "frame-ancestors 'none'" }, + { "default-src 'none'; frame-ancestors 'self'", + "default-src 'none'; frame-ancestors 'self'" }, + { "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 'self'; frame-ancestors 'self'" }, + { "frame-ancestors http://bar.com/foo.png", + "frame-ancestors http://bar.com/foo.png" }, + // clang-format on + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1)); +} + +// ==================== TestBadGeneratedPolicies ==================== + +TEST(CSPParser, BadGeneratedPolicies) +{ + static const PolicyTest policies[] = { + // clang-format off + { "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", ""}, + // clang-format on + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_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[] = { + // clang-format off + { "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////////////" }, + // clang-format on + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_NS_SUCCEEDED(runTestSuite(policies, policyCount, 1)); +} + +// ============== TestBadGeneratedPoliciesForPathHandling ============ + +TEST(CSPParser, BadGeneratedPoliciesForPathHandling) +{ + static const PolicyTest policies[] = { + // clang-format off + { "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'" }, + // clang-format on + }; + + uint32_t policyCount = sizeof(policies) / sizeof(PolicyTest); + ASSERT_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/TestFilenameEvalParser.cpp b/dom/security/test/gtest/TestFilenameEvalParser.cpp new file mode 100644 index 0000000000..60683007ca --- /dev/null +++ b/dom/security/test/gtest/TestFilenameEvalParser.cpp @@ -0,0 +1,453 @@ +/* -*- 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> + +#include "nsContentSecurityUtils.h" +#include "nsStringFwd.h" + +#include "mozilla/ExtensionPolicyService.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/SimpleGlobalObject.h" +#include "mozilla/extensions/WebExtensionPolicy.h" + +static constexpr auto kChromeURI = "chromeuri"_ns; +static constexpr auto kResourceURI = "resourceuri"_ns; +static constexpr auto kBlobUri = "bloburi"_ns; +static constexpr auto kDataUri = "dataurl"_ns; +static constexpr auto kAboutUri = "abouturi"_ns; +static constexpr auto kSingleString = "singlestring"_ns; +static constexpr auto kMozillaExtensionFile = "mozillaextension_file"_ns; +static constexpr auto kExtensionURI = "extension_uri"_ns; +static constexpr auto kSuspectedUserChromeJS = "suspectedUserChromeJS"_ns; +#if defined(XP_WIN) +static constexpr auto kSanitizedWindowsURL = "sanitizedWindowsURL"_ns; +static constexpr auto kSanitizedWindowsPath = "sanitizedWindowsPath"_ns; +#endif +static constexpr auto kOther = "other"_ns; + +#define ASSERT_AND_PRINT(first, second, condition) \ + fprintf(stderr, "First: %s\n", first.get()); \ + fprintf(stderr, "Second: %s\n", NS_ConvertUTF16toUTF8(second).get()); \ + ASSERT_TRUE((condition)); +// Usage: ASSERT_AND_PRINT(ret.first, ret.second.value(), ... + +#define ASSERT_AND_PRINT_FIRST(first, condition) \ + fprintf(stderr, "First: %s\n", (first).get()); \ + ASSERT_TRUE((condition)); +// Usage: ASSERT_AND_PRINT_FIRST(ret.first, ... + +TEST(FilenameEvalParser, ResourceChrome) +{ + { + constexpr auto str = u"chrome://firegestures/content/browser.js"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kChromeURI && ret.second.isSome() && + ret.second.value() == str); + } + { + constexpr auto str = u"resource://firegestures/content/browser.js"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kResourceURI && ret.second.isSome() && + ret.second.value() == str); + } +} + +TEST(FilenameEvalParser, BlobData) +{ + { + constexpr auto str = u"blob://000-000"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kBlobUri && !ret.second.isSome()); + } + { + constexpr auto str = u"blob:000-000"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kBlobUri && !ret.second.isSome()); + } + { + constexpr auto str = u"data://blahblahblah"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kDataUri && !ret.second.isSome()); + } + { + constexpr auto str = u"data:blahblahblah"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kDataUri && !ret.second.isSome()); + } +} + +TEST(FilenameEvalParser, MozExtension) +{ + { // Test shield.mozilla.org replacing + constexpr auto str = + u"jar:file:///c:/users/bob/appdata/roaming/mozilla/firefox/profiles/" + u"foo/" + "extensions/federated-learning@shield.mozilla.org.xpi!/experiments/" + "study/api.js"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kMozillaExtensionFile && + ret.second.value() == + u"federated-learning@s!/experiments/study/api.js"_ns); + } + { // Test mozilla.org replacing + constexpr auto str = + u"jar:file:///c:/users/bob/appdata/roaming/mozilla/firefox/profiles/" + u"foo/" + "extensions/federated-learning@shigeld.mozilla.org.xpi!/experiments/" + "study/api.js"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE( + ret.first == kMozillaExtensionFile && + ret.second.value() == + nsLiteralString( + u"federated-learning@shigeld.m!/experiments/study/api.js")); + } + { // Test truncating + constexpr auto str = + u"jar:file:///c:/users/bob/appdata/roaming/mozilla/firefox/profiles/" + u"foo/" + "extensions/federated-learning@shigeld.mozilla.org.xpi!/experiments/" + "study/apiiiiiiiiiiiiiiiiiiiiiiiiiiiiii.js"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kMozillaExtensionFile && + ret.second.value() == + u"federated-learning@shigeld.m!/experiments/" + "study/apiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"_ns); + } +} + +TEST(FilenameEvalParser, UserChromeJS) +{ + { + constexpr auto str = u"firegestures/content/browser.uc.js"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kSuspectedUserChromeJS && !ret.second.isSome()); + } + { + constexpr auto str = u"firegestures/content/browser.uc.js?"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kSuspectedUserChromeJS && !ret.second.isSome()); + } + { + constexpr auto str = u"firegestures/content/browser.uc.js?243244224"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kSuspectedUserChromeJS && !ret.second.isSome()); + } + { + constexpr auto str = + u"file:///b:/fxprofiles/mark/chrome/" + "addbookmarkherewithmiddleclick.uc.js?1558444389291"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kSuspectedUserChromeJS && !ret.second.isSome()); + } +} + +TEST(FilenameEvalParser, SingleFile) +{ + { + constexpr auto str = u"browser.uc.js?2456"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kSingleString && ret.second.isSome() && + ret.second.value() == str); + } + { + constexpr auto str = u"debugger"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kSingleString && ret.second.isSome() && + ret.second.value() == str); + } +} + +TEST(FilenameEvalParser, Other) +{ + { + constexpr auto str = u"firegestures--content"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kOther && !ret.second.isSome()); + } + { + constexpr auto str = u"gallop://thing/fire"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); +#if defined(XP_WIN) + ASSERT_TRUE(ret.first == kSanitizedWindowsURL && + ret.second.value() == u"gallop"_ns); +#else + ASSERT_TRUE(ret.first == kOther && !ret.second.isSome()); +#endif + } + { + constexpr auto str = u"gallop://fire"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); +#if defined(XP_WIN) + ASSERT_TRUE(ret.first == kSanitizedWindowsURL && + ret.second.value() == u"gallop"_ns); +#else + ASSERT_TRUE(ret.first == kOther && !ret.second.isSome()); +#endif + } + { + constexpr auto str = u"firegestures/content"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); +#if defined(XP_WIN) + ASSERT_TRUE(ret.first == kSanitizedWindowsPath && + ret.second.value() == u"content"_ns); +#else + ASSERT_TRUE(ret.first == kOther && !ret.second.isSome()); +#endif + } + { + constexpr auto str = u"firegestures\\content"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); +#if defined(XP_WIN) + ASSERT_TRUE(ret.first == kSanitizedWindowsPath && + ret.second.value() == u"content"_ns); +#else + ASSERT_TRUE(ret.first == kOther && !ret.second.isSome()); +#endif + } + { + constexpr auto str = u"/home/tom/files/thing"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); +#if defined(XP_WIN) + ASSERT_TRUE(ret.first == kSanitizedWindowsPath && + ret.second.value() == u"thing"_ns); +#else + ASSERT_TRUE(ret.first == kOther && !ret.second.isSome()); +#endif + } + { + constexpr auto str = u"file://c/uers/tom/file.txt"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); +#if defined(XP_WIN) + ASSERT_TRUE(ret.first == kSanitizedWindowsURL && + ret.second.value() == u"file://.../file.txt"_ns); +#else + ASSERT_TRUE(ret.first == kOther && !ret.second.isSome()); +#endif + } + { + constexpr auto str = u"c:/uers/tom/file.txt"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); +#if defined(XP_WIN) + ASSERT_TRUE(ret.first == kSanitizedWindowsPath && + ret.second.value() == u"file.txt"_ns); +#else + ASSERT_TRUE(ret.first == kOther && !ret.second.isSome()); +#endif + } + { + constexpr auto str = u"http://example.com/"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); +#if defined(XP_WIN) + ASSERT_TRUE(ret.first == kSanitizedWindowsURL && + ret.second.value() == u"http"_ns); +#else + ASSERT_TRUE(ret.first == kOther && !ret.second.isSome()); +#endif + } + { + constexpr auto str = u"http://example.com/thing.html"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); +#if defined(XP_WIN) + ASSERT_TRUE(ret.first == kSanitizedWindowsURL && + ret.second.value() == u"http"_ns); +#else + ASSERT_TRUE(ret.first == kOther && !ret.second.isSome()); +#endif + } +} + +TEST(FilenameEvalParser, WebExtensionPathParser) +{ + { + // Set up an Extension and register it so we can test against it. + mozilla::dom::AutoJSAPI jsAPI; + ASSERT_TRUE(jsAPI.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsAPI.cx(); + + mozilla::dom::GlobalObject go(cx, xpc::PrivilegedJunkScope()); + auto* wEI = new mozilla::extensions::WebExtensionInit(); + + JS::Rooted<JSObject*> func( + cx, (JSObject*)JS_NewFunction(cx, (JSNative)1, 0, 0, "customMethodA")); + JS::Rooted<JSObject*> tempGlobalRoot(cx, JS::CurrentGlobalOrNull(cx)); + wEI->mLocalizeCallback = new mozilla::dom::WebExtensionLocalizeCallback( + cx, func, tempGlobalRoot, nullptr); + + wEI->mAllowedOrigins = + mozilla::dom::OwningMatchPatternSetOrStringSequence(); + nsString* slotPtr = + wEI->mAllowedOrigins.SetAsStringSequence().AppendElement( + mozilla::fallible); + ASSERT_TRUE(slotPtr != nullptr); + nsString& slot = *slotPtr; + slot.Truncate(); + slot = u"http://example.com"_ns; + + wEI->mName = u"gtest Test Extension"_ns; + wEI->mId = u"gtesttestextension@mozilla.org"_ns; + wEI->mBaseURL = u"file://foo"_ns; + wEI->mMozExtensionHostname = "e37c3c08-beac-a04b-8032-c4f699a1a856"_ns; + + mozilla::ErrorResult eR; + RefPtr<mozilla::WebExtensionPolicy> w = + mozilla::extensions::WebExtensionPolicy::Constructor(go, *wEI, eR); + w->SetActive(true, eR); + + constexpr auto str = + u"moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856/path/to/file.js"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, true); + + ASSERT_TRUE(ret.first == kExtensionURI && + ret.second.value() == + u"moz-extension://[gtesttestextension@mozilla.org: " + "gtest Test Extension]P=0/path/to/file.js"_ns); + + w->SetActive(false, eR); + + delete wEI; + } + { + // Set up an Extension and register it so we can test against it. + mozilla::dom::AutoJSAPI jsAPI; + ASSERT_TRUE(jsAPI.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsAPI.cx(); + + mozilla::dom::GlobalObject go(cx, xpc::PrivilegedJunkScope()); + auto wEI = new mozilla::extensions::WebExtensionInit(); + + JS::Rooted<JSObject*> func( + cx, (JSObject*)JS_NewFunction(cx, (JSNative)1, 0, 0, "customMethodA")); + JS::Rooted<JSObject*> tempGlobalRoot(cx, JS::CurrentGlobalOrNull(cx)); + wEI->mLocalizeCallback = new mozilla::dom::WebExtensionLocalizeCallback( + cx, func, tempGlobalRoot, NULL); + + wEI->mAllowedOrigins = + mozilla::dom::OwningMatchPatternSetOrStringSequence(); + nsString* slotPtr = + wEI->mAllowedOrigins.SetAsStringSequence().AppendElement( + mozilla::fallible); + nsString& slot = *slotPtr; + slot.Truncate(); + slot = u"http://example.com"_ns; + + wEI->mName = u"gtest Test Extension"_ns; + wEI->mId = u"gtesttestextension@mozilla.org"_ns; + wEI->mBaseURL = u"file://foo"_ns; + wEI->mMozExtensionHostname = "e37c3c08-beac-a04b-8032-c4f699a1a856"_ns; + wEI->mIsPrivileged = true; + + mozilla::ErrorResult eR; + RefPtr<mozilla::WebExtensionPolicy> w = + mozilla::extensions::WebExtensionPolicy::Constructor(go, *wEI, eR); + w->SetActive(true, eR); + + constexpr auto str = + u"moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856/path/to/file.js"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, true); + + ASSERT_TRUE(ret.first == kExtensionURI && + ret.second.value() == + u"moz-extension://[gtesttestextension@mozilla.org: " + "gtest Test Extension]P=1/path/to/file.js"_ns); + + w->SetActive(false, eR); + + delete wEI; + } + { + constexpr auto str = + u"moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856/path/to/file.js"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kExtensionURI && !ret.second.isSome()); + } + { + constexpr auto str = + u"moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856/file.js"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, true); + ASSERT_TRUE( + ret.first == kExtensionURI && + ret.second.value() == + nsLiteralString( + u"moz-extension://[failed finding addon by host]/file.js")); + } + { + constexpr auto str = + u"moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856/path/to/" + "file.js?querystringx=6"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, true); + ASSERT_TRUE(ret.first == kExtensionURI && + ret.second.value() == + u"moz-extension://[failed finding addon " + "by host]/path/to/file.js"_ns); + } +} + +TEST(FilenameEvalParser, AboutPageParser) +{ + { + constexpr auto str = u"about:about"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kAboutUri && + ret.second.value() == u"about:about"_ns); + } + { + constexpr auto str = u"about:about?hello"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kAboutUri && + ret.second.value() == u"about:about"_ns); + } + { + constexpr auto str = u"about:about#mom"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kAboutUri && + ret.second.value() == u"about:about"_ns); + } + { + constexpr auto str = u"about:about?hello=there#mom"_ns; + FilenameTypeAndDetails ret = + nsContentSecurityUtils::FilenameToFilenameType(str, false); + ASSERT_TRUE(ret.first == kAboutUri && + ret.second.value() == u"about:about"_ns); + } +} diff --git a/dom/security/test/gtest/TestSecureContext.cpp b/dom/security/test/gtest/TestSecureContext.cpp new file mode 100644 index 0000000000..189bd0b64b --- /dev/null +++ b/dom/security/test/gtest/TestSecureContext.cpp @@ -0,0 +1,122 @@ +/* -*- 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> + +#include "nsContentSecurityManager.h" +#include "nsContentUtils.h" +#include "nsIPrincipal.h" +#include "nsScriptSecurityManager.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/Preferences.h" + +using namespace mozilla; + +static const uint32_t kURIMaxLength = 64; + +struct TestExpectations { + char uri[kURIMaxLength]; + bool expectedResult; +}; + +class MOZ_RAII AutoRestoreBoolPref final { + public: + AutoRestoreBoolPref(const char* aPref, bool aValue) : mPref(aPref) { + Preferences::GetBool(mPref, &mOldValue); + Preferences::SetBool(mPref, aValue); + } + + ~AutoRestoreBoolPref() { Preferences::SetBool(mPref, mOldValue); } + + private: + const char* mPref = nullptr; + bool mOldValue = false; +}; + +// ============================= TestDirectives ======================== + +TEST(SecureContext, IsOriginPotentiallyTrustworthyWithContentPrincipal) +{ + // boolean isOriginPotentiallyTrustworthy(in nsIPrincipal aPrincipal); + + AutoRestoreBoolPref savedPref("network.proxy.allow_hijacking_localhost", + false); + + static const TestExpectations uris[] = { + {"http://example.com/", false}, + {"https://example.com/", true}, + {"ws://example.com/", false}, + {"wss://example.com/", true}, + {"file:///xyzzy", true}, + {"ftp://example.com", false}, + {"about:config", false}, + {"http://localhost", true}, + {"http://localhost.localhost", true}, + {"http://a.b.c.d.e.localhost", true}, + {"http://xyzzy.localhost", true}, + {"http://127.0.0.1", true}, + {"http://127.0.0.2", true}, + {"http://127.1.0.1", true}, + {"http://128.0.0.1", false}, + {"http://[::1]", true}, + {"http://[::ffff:127.0.0.1]", false}, + {"http://[::ffff:127.0.0.2]", false}, + {"http://[::ffff:7f00:1]", false}, + {"http://[::ffff:7f00:2]", false}, + {"resource://xyzzy", true}, + {"moz-extension://xyzzy", true}, + {"data:data:text/plain;charset=utf-8;base64,eHl6enk=", false}, + {"blob://unique-id", false}, + {"mailto:foo@bar.com", false}, + {"moz-icon://example.com", false}, + {"javascript:42", false}, + }; + + uint32_t numExpectations = sizeof(uris) / sizeof(TestExpectations); + nsCOMPtr<nsIContentSecurityManager> csManager = + do_GetService(NS_CONTENTSECURITYMANAGER_CONTRACTID); + ASSERT_TRUE(!!csManager); + + nsresult rv; + for (uint32_t i = 0; i < numExpectations; i++) { + nsCOMPtr<nsIPrincipal> prin; + nsAutoCString uri(uris[i].uri); + rv = nsScriptSecurityManager::GetScriptSecurityManager() + ->CreateContentPrincipalFromOrigin(uri, getter_AddRefs(prin)); + ASSERT_EQ(rv, NS_OK); + bool isPotentiallyTrustworthy = prin->GetIsOriginPotentiallyTrustworthy(); + ASSERT_EQ(isPotentiallyTrustworthy, uris[i].expectedResult) + << uris[i].uri << uris[i].expectedResult; + } +} + +TEST(SecureContext, IsOriginPotentiallyTrustworthyWithSystemPrincipal) +{ + RefPtr<nsScriptSecurityManager> ssManager = + nsScriptSecurityManager::GetScriptSecurityManager(); + ASSERT_TRUE(!!ssManager); + nsCOMPtr<nsIPrincipal> sysPrin = nsContentUtils::GetSystemPrincipal(); + bool isPotentiallyTrustworthy = sysPrin->GetIsOriginPotentiallyTrustworthy(); + ASSERT_TRUE(isPotentiallyTrustworthy); +} + +TEST(SecureContext, IsOriginPotentiallyTrustworthyWithNullPrincipal) +{ + RefPtr<nsScriptSecurityManager> ssManager = + nsScriptSecurityManager::GetScriptSecurityManager(); + ASSERT_TRUE(!!ssManager); + + RefPtr<NullPrincipal> nullPrin = + NullPrincipal::CreateWithoutOriginAttributes(); + bool isPotentiallyTrustworthy; + nsresult rv = + nullPrin->GetIsOriginPotentiallyTrustworthy(&isPotentiallyTrustworthy); + ASSERT_EQ(rv, NS_OK); + ASSERT_TRUE(!isPotentiallyTrustworthy); +} diff --git a/dom/security/test/gtest/TestSmartCrashTrimmer.cpp b/dom/security/test/gtest/TestSmartCrashTrimmer.cpp new file mode 100644 index 0000000000..d2238c0d75 --- /dev/null +++ b/dom/security/test/gtest/TestSmartCrashTrimmer.cpp @@ -0,0 +1,44 @@ +/* -*- 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 <stdlib.h> +#include <stdio.h> +#include <string.h> + +#include "nsContentSecurityUtils.h" +#include "nsTString.h" +#include "nsStringFwd.h" +#include "mozilla/Sprintf.h" + +#define ASSERT_STRCMP(first, second) ASSERT_TRUE(strcmp(first, second) == 0); + +#define ASSERT_STRCMP_AND_PRINT(first, second) \ + fprintf(stderr, "First: %s\n", first); \ + fprintf(stderr, "Second: %s\n", second); \ + fprintf(stderr, "strcmp = %i\n", strcmp(first, second)); \ + ASSERT_EQUAL(first, second); + +TEST(SmartCrashTrimmer, Test) +{ + static_assert(sPrintfCrashReasonSize == 1024); + { + auto ret = nsContentSecurityUtils::SmartFormatCrashString( + std::string(1025, '.').c_str()); + ASSERT_EQ(strlen(ret), 1023ul); + } + + { + auto ret = nsContentSecurityUtils::SmartFormatCrashString( + std::string(1025, '.').c_str(), std::string(1025, 'A').c_str(), + "Hello %s world %s!"); + char expected[1025]; + SprintfLiteral(expected, "Hello %s world AAAAAAAAAAAAAAAAAAAAAAAAA!", + std::string(984, '.').c_str()); + ASSERT_STRCMP(ret.get(), expected); + } +} diff --git a/dom/security/test/gtest/TestUnexpectedPrivilegedLoads.cpp b/dom/security/test/gtest/TestUnexpectedPrivilegedLoads.cpp new file mode 100644 index 0000000000..772e4bd353 --- /dev/null +++ b/dom/security/test/gtest/TestUnexpectedPrivilegedLoads.cpp @@ -0,0 +1,305 @@ +/* -*- 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 "core/TelemetryEvent.h" +#include "gtest/gtest.h" +#include "js/Array.h" // JS::GetArrayLength +#include "js/PropertyAndElement.h" // JS_GetElement, JS_GetProperty +#include "js/TypeDecls.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" +#include "TelemetryFixture.h" +#include "TelemetryTestHelpers.h" + +#include <string.h> +#include <stdlib.h> +#include "nsContentSecurityManager.h" +#include "nsContentSecurityUtils.h" +#include "nsContentUtils.h" +#include "nsIContentPolicy.h" +#include "nsILoadInfo.h" +#include "nsNetUtil.h" +#include "nsStringFwd.h" + +using namespace mozilla; +using namespace TelemetryTestHelpers; + +extern Atomic<bool, mozilla::Relaxed> sJSHacksChecked; +extern Atomic<bool, mozilla::Relaxed> sJSHacksPresent; +extern Atomic<bool, mozilla::Relaxed> sCSSHacksChecked; +extern Atomic<bool, mozilla::Relaxed> sCSSHacksPresent; + +TEST_F(TelemetryTestFixture, UnexpectedPrivilegedLoadsTelemetryTest) { + // Disable JS/CSS Hacks Detection, which would consider this current profile + // as uninteresting for our measurements: + bool origJSHacksPresent = sJSHacksPresent; + bool origJSHacksChecked = sJSHacksChecked; + sJSHacksPresent = false; + sJSHacksChecked = true; + bool origCSSHacksPresent = sCSSHacksPresent; + bool origCSSHacksChecked = sCSSHacksChecked; + sCSSHacksPresent = false; + sCSSHacksChecked = true; + + struct testResults { + nsCString fileinfo; + nsCString extraValueContenttype; + nsCString extraValueRemotetype; + nsCString extraValueFiledetails; + nsCString extraValueRedirects; + }; + + struct testCasesAndResults { + nsCString urlstring; + nsContentPolicyType contentType; + nsCString remoteType; + testResults expected; + }; + + AutoJSContextWithGlobal cx(mCleanGlobal); + // Make sure we don't look at events from other tests. + Unused << mTelemetry->ClearEvents(); + + // required for telemetry lookups + constexpr auto category = "security"_ns; + constexpr auto method = "unexpectedload"_ns; + constexpr auto object = "systemprincipal"_ns; + constexpr auto extraKeyContenttype = "contenttype"_ns; + constexpr auto extraKeyRemotetype = "remotetype"_ns; + constexpr auto extraKeyFiledetails = "filedetails"_ns; + constexpr auto extraKeyRedirects = "redirects"_ns; + + // some cases from TestFilenameEvalParser + // no need to replicate all scenarios?! + testCasesAndResults myTestCases[] = { + {"chrome://firegestures/content/browser.js"_ns, + nsContentPolicyType::TYPE_SCRIPT, + "web"_ns, + {"chromeuri"_ns, "TYPE_SCRIPT"_ns, "web"_ns, + "chrome://firegestures/content/browser.js"_ns, ""_ns}}, + {"resource://firegestures/content/browser.js"_ns, + nsContentPolicyType::TYPE_SCRIPT, + "web"_ns, + {"resourceuri"_ns, "TYPE_SCRIPT"_ns, "web"_ns, + "resource://firegestures/content/browser.js"_ns, ""_ns}}, + {// test that we don't report blob details + // ..and test that we strip of URLs from remoteTypes + "blob://000-000"_ns, + nsContentPolicyType::TYPE_SCRIPT, + "webIsolated=https://blob.example/"_ns, + {"bloburi"_ns, "TYPE_SCRIPT"_ns, "webIsolated"_ns, "unknown"_ns, ""_ns}}, + {// test for cases where finalURI is null, due to a broken nested URI + // .. like malformed moz-icon URLs + "moz-icon:blahblah"_ns, + nsContentPolicyType::TYPE_DOCUMENT, + "web"_ns, + {"other"_ns, "TYPE_DOCUMENT"_ns, "web"_ns, "unknown"_ns, ""_ns}}, + {// we dont report data urls + // ..and test that we strip of URLs from remoteTypes + "data://blahblahblah"_ns, + nsContentPolicyType::TYPE_SCRIPT, + "webCOOP+COEP=https://data.example"_ns, + {"dataurl"_ns, "TYPE_SCRIPT"_ns, "webCOOP+COEP"_ns, "unknown"_ns, + ""_ns}}, + {// handle data URLs for webextension content scripts differently + // .. by noticing their annotation + "data:text/css;extension=style;charset=utf-8,/* some css here */"_ns, + nsContentPolicyType::TYPE_STYLESHEET, + "web"_ns, + {"dataurl-extension-contentstyle"_ns, "TYPE_STYLESHEET"_ns, "web"_ns, + "unknown"_ns, ""_ns}}, + {// we only report file URLs on windows, where we can easily sanitize + "file://c/users/tom/file.txt"_ns, + nsContentPolicyType::TYPE_SCRIPT, + "web"_ns, + { +#if defined(XP_WIN) + "sanitizedWindowsURL"_ns, "TYPE_SCRIPT"_ns, "web"_ns, + "file://.../file.txt"_ns, ""_ns + +#else + "other"_ns, "TYPE_SCRIPT"_ns, "web"_ns, "unknown"_ns, ""_ns +#endif + }}, + {// test for one redirect + "moz-extension://abcdefab-1234-4321-0000-abcdefabcdef/js/assets.js"_ns, + nsContentPolicyType::TYPE_SCRIPT, + "web"_ns, + {"extension_uri"_ns, "TYPE_SCRIPT"_ns, "web"_ns, + // the extension-id is made-up, so the extension will report failure + "moz-extension://[failed finding addon by host]/js/assets.js"_ns, + "https"_ns}}, + {// test for cases where finalURI is empty + ""_ns, + nsContentPolicyType::TYPE_STYLESHEET, + "web"_ns, + {"other"_ns, "TYPE_STYLESHEET"_ns, "web"_ns, "unknown"_ns, ""_ns}}, + {// test for cases where finalURI is null, due to the struct layout, we'll + // override the URL with nullptr in loop below. + "URLWillResultInNullPtr"_ns, + nsContentPolicyType::TYPE_SCRIPT, + "web"_ns, + {"other"_ns, "TYPE_SCRIPT"_ns, "web"_ns, "unknown"_ns, ""_ns}}, + }; + + int i = 0; + for (auto const& currentTest : myTestCases) { + nsresult rv; + nsCOMPtr<nsIURI> uri; + + // special-casing for a case where the uri is null + if (!currentTest.urlstring.Equals("URLWillResultInNullPtr")) { + NS_NewURI(getter_AddRefs(uri), currentTest.urlstring); + } + + // We can't create channels for chrome: URLs unless they are in a chrome + // registry that maps them into the actual destination URL (usually + // file://). It seems that gtest don't have chrome manifest registered, so + // we'll use a mockChannel with a mockUri. + nsCOMPtr<nsIURI> mockUri; + rv = NS_NewURI(getter_AddRefs(mockUri), "http://example.com"_ns); + ASSERT_EQ(rv, NS_OK) << "Could not create mockUri"; + nsCOMPtr<nsIChannel> mockChannel; + nsCOMPtr<nsIIOService> service = do_GetIOService(); + if (!service) { + ASSERT_TRUE(false) + << "Couldn't initialize IOService"; + } + rv = service->NewChannelFromURI( + mockUri, nullptr, nsContentUtils::GetSystemPrincipal(), + nsContentUtils::GetSystemPrincipal(), 0, currentTest.contentType, + getter_AddRefs(mockChannel)); + ASSERT_EQ(rv, NS_OK) << "Could not create a mock channel"; + nsCOMPtr<nsILoadInfo> mockLoadInfo = mockChannel->LoadInfo(); + + // We're adding a redirect entry for one specific test + if (currentTest.urlstring.EqualsASCII( + "moz-extension://abcdefab-1234-4321-0000-abcdefabcdef/js/" + "assets.js")) { + nsCOMPtr<nsIURI> redirUri; + NS_NewURI(getter_AddRefs(redirUri), + "https://www.analytics.example/analytics.js"_ns); + nsCOMPtr<nsIPrincipal> redirPrincipal = + BasePrincipal::CreateContentPrincipal(redirUri, OriginAttributes()); + nsCOMPtr<nsIChannel> redirectChannel; + Unused << service->NewChannelFromURI(redirUri, nullptr, redirPrincipal, + nullptr, 0, currentTest.contentType, + getter_AddRefs(redirectChannel)); + + mockLoadInfo->AppendRedirectHistoryEntry(redirectChannel, false); + } + + // this will record the event + nsContentSecurityManager::MeasureUnexpectedPrivilegedLoads( + mockLoadInfo, uri, currentTest.remoteType); + + // let's inspect the recorded events + + JS::Rooted<JS::Value> eventsSnapshot(cx.GetJSContext()); + GetEventSnapshot(cx.GetJSContext(), &eventsSnapshot); + + ASSERT_TRUE(EventPresent(cx.GetJSContext(), eventsSnapshot, category, + method, object)) + << "Test event with value and extra must be present."; + + // Convert eventsSnapshot into array/object + JSContext* aCx = cx.GetJSContext(); + JS::Rooted<JSObject*> arrayObj(aCx, &eventsSnapshot.toObject()); + + JS::Rooted<JS::Value> eventRecord(aCx); + ASSERT_TRUE(JS_GetElement(aCx, arrayObj, i++, &eventRecord)) + << "Must be able to get record."; // record is already undefined :-/ + + ASSERT_TRUE(!eventRecord.isUndefined()) + << "eventRecord should not be undefined"; + + JS::Rooted<JSObject*> recordArray(aCx, &eventRecord.toObject()); + uint32_t recordLength; + ASSERT_TRUE(JS::GetArrayLength(aCx, recordArray, &recordLength)) + << "Event record array must have length."; + ASSERT_TRUE(recordLength == 6) + << "Event record must have 6 elements."; + + JS::Rooted<JS::Value> str(aCx); + nsAutoJSString jsStr; + // The fileinfo string is at index 4 + ASSERT_TRUE(JS_GetElement(aCx, recordArray, 4, &str)) + << "Must be able to get value."; + ASSERT_TRUE(jsStr.init(aCx, str)) + << "Value must be able to be init'd to a jsstring."; + + ASSERT_STREQ(NS_ConvertUTF16toUTF8(jsStr).get(), + currentTest.expected.fileinfo.get()) + << "Reported fileinfo '" << NS_ConvertUTF16toUTF8(jsStr).get() + << " 'equals expected value: " << currentTest.expected.fileinfo.get(); + + // Extra is at index 5 + JS::Rooted<JS::Value> obj(aCx); + ASSERT_TRUE(JS_GetElement(aCx, recordArray, 5, &obj)) + << "Must be able to get extra data"; + JS::Rooted<JSObject*> extraObj(aCx, &obj.toObject()); + // looking at remotetype extra for content type + JS::Rooted<JS::Value> extraValC(aCx); + ASSERT_TRUE( + JS_GetProperty(aCx, extraObj, extraKeyContenttype.get(), &extraValC)) + << "Must be able to get the extra key's value for contenttype"; + ASSERT_TRUE(jsStr.init(aCx, extraValC)) + << "Extra value contenttype must be able to be init'd to a jsstring."; + ASSERT_STREQ(NS_ConvertUTF16toUTF8(jsStr).get(), + currentTest.expected.extraValueContenttype.get()) + << "Reported value for extra contenttype '" + << NS_ConvertUTF16toUTF8(jsStr).get() + << "' should equals supplied value" + << currentTest.expected.extraValueContenttype.get(); + // and again for remote type + JS::Rooted<JS::Value> extraValP(aCx); + ASSERT_TRUE( + JS_GetProperty(aCx, extraObj, extraKeyRemotetype.get(), &extraValP)) + << "Must be able to get the extra key's value for remotetype"; + ASSERT_TRUE(jsStr.init(aCx, extraValP)) + << "Extra value remotetype must be able to be init'd to a jsstring."; + ASSERT_STREQ(NS_ConvertUTF16toUTF8(jsStr).get(), + currentTest.expected.extraValueRemotetype.get()) + << "Reported value for extra remotetype '" + << NS_ConvertUTF16toUTF8(jsStr).get() + << "' should equals supplied value: " + << currentTest.expected.extraValueRemotetype.get(); + // repeating the same for filedetails extra + JS::Rooted<JS::Value> extraValF(aCx); + ASSERT_TRUE( + JS_GetProperty(aCx, extraObj, extraKeyFiledetails.get(), &extraValF)) + << "Must be able to get the extra key's value for filedetails"; + ASSERT_TRUE(jsStr.init(aCx, extraValF)) + << "Extra value filedetails must be able to be init'd to a jsstring."; + ASSERT_STREQ(NS_ConvertUTF16toUTF8(jsStr).get(), + currentTest.expected.extraValueFiledetails.get()) + << "Reported value for extra filedetails '" + << NS_ConvertUTF16toUTF8(jsStr).get() << "'should equals supplied value" + << currentTest.expected.extraValueFiledetails.get(); + // checking the extraKeyRedirects match + JS::Rooted<JS::Value> extraValRedirects(aCx); + ASSERT_TRUE(JS_GetProperty(aCx, extraObj, extraKeyRedirects.get(), + &extraValRedirects)) + << "Must be able to get the extra value for redirects"; + ASSERT_TRUE(jsStr.init(aCx, extraValRedirects)) + << "Extra value redirects must be able to be init'd to a jsstring"; + ASSERT_STREQ(NS_ConvertUTF16toUTF8(jsStr).get(), + currentTest.expected.extraValueRedirects.get()) + << "Reported value for extra redirect '" + << NS_ConvertUTF16toUTF8(jsStr).get() + << "' should equals supplied value: " + << currentTest.expected.extraValueRedirects.get(); + } + + // Re-store JS/CSS hacks detection state + sJSHacksPresent = origJSHacksPresent; + sJSHacksChecked = origJSHacksChecked; + sCSSHacksPresent = origCSSHacksPresent; + sCSSHacksChecked = origCSSHacksChecked; +} diff --git a/dom/security/test/gtest/moz.build b/dom/security/test/gtest/moz.build new file mode 100644 index 0000000000..c9ab4dcece --- /dev/null +++ b/dom/security/test/gtest/moz.build @@ -0,0 +1,25 @@ +# -*- 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", + "TestFilenameEvalParser.cpp", + "TestSecureContext.cpp", + "TestSmartCrashTrimmer.cpp", +] + +if CONFIG["OS_TARGET"] != "Android": + UNIFIED_SOURCES += [ + "TestUnexpectedPrivilegedLoads.cpp", + ] + +FINAL_LIBRARY = "xul-gtest" + +LOCAL_INCLUDES += [ + "/caps", + "/toolkit/components/telemetry/", + "/toolkit/components/telemetry/tests/gtest", +] diff --git a/dom/security/test/https-first/browser.ini b/dom/security/test/https-first/browser.ini new file mode 100644 index 0000000000..9e355423dc --- /dev/null +++ b/dom/security/test/https-first/browser.ini @@ -0,0 +1,35 @@ +[browser_httpsfirst.js] +support-files = + file_httpsfirst_timeout_server.sjs +[browser_httpsfirst_console_logging.js] +[browser_upgrade_onion.js] +[browser_mixed_content_console.js] +support-files = + file_mixed_content_console.html +[browser_downgrade_view_source.js] +support-files = file_downgrade_view_source.sjs +[browser_navigation.js] +support-files = file_navigation.html +[browser_httpsfirst_speculative_connect.js] +support-files = file_httpsfirst_speculative_connect.html +[browser_download_attribute.js] +support-files = file_download_attribute.html + file_download_attribute.sjs + +[browser_mixed_content_download.js] +skip-if = win10_2004 && debug # Bug 1723573 +support-files = + download_page.html + download_server.sjs +[browser_slow_download.js] +support-files = + file_slow_download.html + file_slow_download.sjs +[browser_downgrade_mixed_content_auto_upgrade_console.js] +support-files = + file_mixed_content_auto_upgrade.html + pass.png + test.ogv + test.wav +[browser_beforeunload_permit_http.js] +support-files = file_beforeunload_permit_http.html diff --git a/dom/security/test/https-first/browser_beforeunload_permit_http.js b/dom/security/test/https-first/browser_beforeunload_permit_http.js new file mode 100644 index 0000000000..78e19e2846 --- /dev/null +++ b/dom/security/test/https-first/browser_beforeunload_permit_http.js @@ -0,0 +1,205 @@ +"use strict"; + +const { PromptTestUtils } = ChromeUtils.import( + "resource://testing-common/PromptTestUtils.jsm" +); + +const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://nocert.example.com/" +); +/* + * Description of Tests: + * + * Test load page and reload: + * 1. Enable HTTPS-First and the pref to trigger beforeunload by user interaction + * 2. Open an HTTP site. HTTPS-First will try to upgrade it to https - but since it has no cert that try will fail + * 3. Then simulated user interaction and reload the page with a reload flag. + * 4. That should lead to a beforeUnload prompt that asks for users permission to perform reload. HTTPS-First should not try to upgrade the reload again + * + * Test Navigation: + * 1. Enable HTTPS-First and the pref to trigger beforeunload by user interaction + * 2. Open an http site. HTTPS-First will try to upgrade it to https - but since it has no cert for https that try will fail + * 3. Then simulated user interaction and navigate to another http page. Again HTTPS-First will try to upgrade to HTTPS + * 4. This attempted navigation leads to a prompt which askes for permission to leave page - accept it + * 5. Since the site is not using a valid HTTPS cert HTTPS-First will downgrade the request back to HTTP + * 6. User should NOT get asked again for permission to unload + * + * Test Session History Navigation: + * 1. Enable HTTPS-First and the pref to trigger beforeunload by user interaction + * 2. Open an http site. HTTPS-First will try to upgrade it to https - but since it has no cert for https that try will fail + * 3. Then navigate to another http page and simulated a user interaction. + * 4. Trigger a session history navigation by clicking the "back button". + * 5. This attempted navigation leads to a prompt which askes for permission to leave page - accept it + */ +add_setup(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_first", true], + ["dom.require_user_interaction_for_beforeunload", true], + ], + }); +}); +const TESTS = [ + { + name: "Normal Reload (No flag)", + reloadFlag: Ci.nsIWebNavigation.LOAD_FLAGS_NONE, + }, + { + name: "Bypass Cache Reload", + reloadFlag: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE, + }, + { + name: "Bypass Proxy Reload", + reloadFlag: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY, + }, + { + name: "Bypass Cache and Proxy Reload", + reloadFlag: + Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE | + Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY, + }, +]; + +add_task(async function testReloadFlags() { + for (let index = 0; index < TESTS.length; index++) { + const testCase = TESTS[index]; + // The onbeforeunload dialog should appear + let dialogPromise = PromptTestUtils.waitForPrompt(null, { + modalType: Services.prompt.MODAL_TYPE_CONTENT, + promptType: "confirmEx", + }); + let reloadPromise = loadPageAndReload(testCase); + let dialog = await dialogPromise; + Assert.ok(true, "Showed the beforeunload dialog."); + await PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 0 }); + await reloadPromise; + } +}); + +add_task(async function testNavigation() { + // The onbeforeunload dialog should appear + let dialogPromise = PromptTestUtils.waitForPrompt(null, { + modalType: Services.prompt.MODAL_TYPE_CONTENT, + promptType: "confirmEx", + }); + + let openPagePromise = openPage(); + let dialog = await dialogPromise; + Assert.ok(true, "Showed the beforeunload dialog."); + await PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 0 }); + await openPagePromise; +}); + +add_task(async function testSessionHistoryNavigation() { + // The onbeforeunload dialog should appear + let dialogPromise = PromptTestUtils.waitForPrompt(null, { + modalType: Services.prompt.MODAL_TYPE_CONTENT, + promptType: "confirmEx", + }); + + let openPagePromise = loadPagesAndUseBackButton(); + let dialog = await dialogPromise; + Assert.ok(true, "Showed the beforeunload dialog."); + await PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 0 }); + await openPagePromise; +}); + +async function openPage() { + // Open about:blank in a new tab + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async function(browser) { + // Load http page + BrowserTestUtils.loadURI( + browser, + `${TEST_PATH_HTTP}file_beforeunload_permit_http.html` + ); + await BrowserTestUtils.browserLoaded(browser); + // Interact with page such that unload permit will be necessary + await BrowserTestUtils.synthesizeMouse("body", 2, 2, {}, browser); + let hasInteractedWith = await SpecialPowers.spawn( + browser, + [""], + function() { + return content.document.userHasInteracted; + } + ); + + is(true, hasInteractedWith, "Simulated successfully user interaction"); + // And then navigate away to another site which proves that user won't be asked twice to permit a reload (otherwise the test get timed out) + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + BrowserTestUtils.loadURI(browser, "http://self-signed.example.com/"); + await BrowserTestUtils.browserLoaded(browser); + Assert.ok(true, "Navigated successfully."); + } + ); +} + +async function loadPageAndReload(testCase) { + // Load initial site + // Open about:blank in a new tab + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async function(browser) { + BrowserTestUtils.loadURI( + browser, + `${TEST_PATH_HTTP}file_beforeunload_permit_http.html` + ); + await BrowserTestUtils.browserLoaded(browser); + // Interact with page such that unload permit will be necessary + await BrowserTestUtils.synthesizeMouse("body", 2, 2, {}, browser); + + let hasInteractedWith = await SpecialPowers.spawn( + browser, + [""], + function() { + return content.document.userHasInteracted; + } + ); + is(true, hasInteractedWith, "Simulated successfully user interaction"); + BrowserReloadWithFlags(testCase.reloadFlag); + await BrowserTestUtils.browserLoaded(browser); + is(true, true, `reload with flag ${testCase.name} was successful`); + } + ); +} + +async function loadPagesAndUseBackButton() { + // Load initial site + // Open about:blank in a new tab + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async function(browser) { + BrowserTestUtils.loadURI( + browser, + `${TEST_PATH_HTTP}file_beforeunload_permit_http.html` + ); + await BrowserTestUtils.browserLoaded(browser); + + BrowserTestUtils.loadURI( + browser, + `${TEST_PATH_HTTP}file_beforeunload_permit_http.html?getASessionHistoryEntry` + ); + await BrowserTestUtils.browserLoaded(browser); + // Interact with page such that unload permit will be necessary + await BrowserTestUtils.synthesizeMouse("body", 2, 2, {}, browser); + + let hasInteractedWith = await SpecialPowers.spawn( + browser, + [""], + function() { + return content.document.userHasInteracted; + } + ); + is(true, hasInteractedWith, "Simulated successfully user interaction"); + // Go back one site by clicking the back button + info("Clicking back button"); + let backButton = document.getElementById("back-button"); + backButton.click(); + await BrowserTestUtils.browserLoaded(browser); + is(true, true, `Got back successful`); + } + ); +} diff --git a/dom/security/test/https-first/browser_downgrade_mixed_content_auto_upgrade_console.js b/dom/security/test/https-first/browser_downgrade_mixed_content_auto_upgrade_console.js new file mode 100644 index 0000000000..b3d41807bb --- /dev/null +++ b/dom/security/test/https-first/browser_downgrade_mixed_content_auto_upgrade_console.js @@ -0,0 +1,80 @@ +// Bug 1673574 - Improve Console logging for mixed content auto upgrading +"use strict"; + +const testPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://httpsfirst.com" +); + +let tests = [ + { + description: "Top-Level upgrade should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: ["Upgrading insecure request", "to use", "httpsfirst.com"], + }, + { + description: "Top-Level upgrade failure should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: [ + "Upgrading insecure request", + "failed", + "httpsfirst.com", + "Downgrading to", + ], + }, +]; + +const kTestURI = testPath + "file_mixed_content_auto_upgrade.html"; + +add_task(async function() { + // A longer timeout is necessary for this test than the plain mochitests + // due to opening a new tab with the web console. + requestLongerTimeout(4); + + // Enable ML2 and HTTPS-First Mode and register console-listener + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.mixed_content.upgrade_display_content", true], + ["dom.security.https_first", true], + ], + }); + Services.console.registerListener(on_new_message); + // 1. Upgrade page to https:// + await BrowserTestUtils.loadURI(gBrowser.selectedBrowser, kTestURI); + + await BrowserTestUtils.waitForCondition(() => tests.length === 0); + + // Clean up + Services.console.unregisterListener(on_new_message); +}); + +function on_new_message(msgObj) { + const message = msgObj.message; + const logLevel = msgObj.logLevel; + + // The console message is: + // Should only show HTTPS-First messages + + if (message.includes("Mixed Content:")) { + ok( + !message.includes("Upgrading insecure display request"), + "msg included a mixed content upgrade" + ); + } + if (message.includes("HTTPS-First Mode:")) { + for (let i = 0; i < tests.length; i++) { + const testCase = tests[i]; + // Check if log-level matches + if (logLevel !== testCase.expectLogLevel) { + continue; + } + // Check if all substrings are included + if (testCase.expectIncludes.some(str => !message.includes(str))) { + continue; + } + ok(true, testCase.description); + tests.splice(i, 1); + break; + } + } +} diff --git a/dom/security/test/https-first/browser_downgrade_view_source.js b/dom/security/test/https-first/browser_downgrade_view_source.js new file mode 100644 index 0000000000..dee3d8db31 --- /dev/null +++ b/dom/security/test/https-first/browser_downgrade_view_source.js @@ -0,0 +1,53 @@ +// This test ensures that view-source:https falls back to view-source:http +"use strict"; + +const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +const TEST_PATH_HTTPS = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +async function runTest(desc, url, expectedURI, excpectedContent) { + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + let loaded = BrowserTestUtils.browserLoaded(browser, false, null, true); + BrowserTestUtils.loadURI(browser, url); + await loaded; + + await SpecialPowers.spawn( + browser, + [desc, expectedURI, excpectedContent], + async function(desc, expectedURI, excpectedContent) { + let loadedURI = content.document.documentURI; + is(loadedURI, expectedURI, desc); + let loadedContent = content.document.body.textContent; + is(loadedContent, excpectedContent, desc); + } + ); + }); +} + +add_task(async function() { + requestLongerTimeout(2); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", true]], + }); + + await runTest( + "URL with query 'downgrade' should be http:", + `view-source:${TEST_PATH_HTTP}/file_downgrade_view_source.sjs?downgrade`, + `view-source:${TEST_PATH_HTTP}/file_downgrade_view_source.sjs?downgrade`, + "view-source:http://" + ); + + await runTest( + "URL with query 'upgrade' should be https:", + `view-source:${TEST_PATH_HTTP}/file_downgrade_view_source.sjs?upgrade`, + `view-source:${TEST_PATH_HTTPS}/file_downgrade_view_source.sjs?upgrade`, + "view-source:https://" + ); +}); diff --git a/dom/security/test/https-first/browser_download_attribute.js b/dom/security/test/https-first/browser_download_attribute.js new file mode 100644 index 0000000000..21682f7a60 --- /dev/null +++ b/dom/security/test/https-first/browser_download_attribute.js @@ -0,0 +1,129 @@ +"use strict"; + +// Create a uri for an http site +//(in that case a site without cert such that https-first isn't upgrading it) +const insecureTestPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://nocert.example.com" +); +const insecureTestURI = insecureTestPath + "file_download_attribute.html"; + +function promisePanelOpened() { + if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") { + return Promise.resolve(); + } + return BrowserTestUtils.waitForEvent(DownloadsPanel.panel, "popupshown"); +} + +const CONSOLE_UPGRADE_TRY_MESSAGE = "Upgrading insecure request"; +const CONSOLE_ERROR_MESSAGE = "Downgrading to “http†again"; +const DOWNLOAD_PAGE_URL = + "nocert.example.com/browser/dom/security/test/https-first/file_download_attribute.html"; +const DOWNLOAD_LINK_URL = + "nocert.example.com/browser/dom/security/test/https-first/file_download_attribute.sjs"; + +// Verifys that https-first tried to upgrade the download +// - and that the upgrade attempt failed. +// We will receive 4 messages. Two for upgrading and downgrading +// the download page and another two for upgrading and downgrading +// the download. +let msgCounter = 0; +function shouldConsoleTryUpgradeAndError() { + // Waits until CONSOLE_ERROR_MESSAGE was logged. + // Checks if download was tried via http:// + return new Promise((resolve, reject) => { + function listener(msgObj) { + let text = msgObj.message; + // Verify upgrade messages + if ( + text.includes(CONSOLE_UPGRADE_TRY_MESSAGE) && + text.includes("http://") + ) { + if (msgCounter == 0) { + ok( + text.includes(DOWNLOAD_PAGE_URL), + "Tries to upgrade nocert example to https" + ); + } else { + ok( + text.includes(DOWNLOAD_LINK_URL), + "Tries to upgrade download to https" + ); + } + msgCounter++; + } + // Verify downgrade messages + if (text.includes(CONSOLE_ERROR_MESSAGE) && msgCounter > 0) { + if (msgCounter == 1) { + ok( + text.includes("https://" + DOWNLOAD_PAGE_URL), + "Downgrades nocert example to http" + ); + msgCounter++; + } else { + ok( + text.includes("https://" + DOWNLOAD_LINK_URL), + "Downgrades download to http" + ); + Services.console.unregisterListener(listener); + resolve(); + } + } + } + Services.console.registerListener(listener); + }); +} + +// Test https-first download of an html file from an http site. +// Test description: +// 1. https-first tries to upgrade site to https +// 2. upgrade fails because site has no certificate +// 3. https-first downgrades to http and starts download via http +// 4. Successfully completes download +add_task(async function test_with_downloads_pref_enabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_first", true], + // ensure that download panel gets opened - Bug 1746434 + ["browser.download.improvements_to_download_panel", true], + ], + }); + let checkPromise = shouldConsoleTryUpgradeAndError(); + let downloadsPanelPromise = promisePanelOpened(); + let downloadsPromise = Downloads.getList(Downloads.PUBLIC); + + BrowserTestUtils.loadURI(gBrowser, insecureTestURI); + // wait for downloadsPanel to open before continuing with test + await downloadsPanelPromise; + let downloadList = await downloadsPromise; + await checkPromise; + is(DownloadsPanel.isPanelShowing, true, "DownloadsPanel should be open."); + is( + downloadList._downloads.length, + 1, + "File should be successfully downloaded." + ); + + let [download] = downloadList._downloads; + is(download.contentType, "text/html", "File contentType should be correct."); + // ensure https-first didn't upgrade the scheme. + is( + download.source.url, + insecureTestPath + "file_download_attribute.sjs", + "Scheme should be http." + ); + + info("cleaning up downloads"); + try { + if (Services.appinfo.OS === "WINNT") { + // We need to make the file writable to delete it on Windows. + await IOUtils.setPermissions(download.target.path, 0o600); + } + await IOUtils.remove(download.target.path); + } catch (error) { + info("The file " + download.target.path + " is not removed, " + error); + } + + await downloadList.remove(download); + await download.finalize(); +}); diff --git a/dom/security/test/https-first/browser_httpsfirst.js b/dom/security/test/https-first/browser_httpsfirst.js new file mode 100644 index 0000000000..7f484f87de --- /dev/null +++ b/dom/security/test/https-first/browser_httpsfirst.js @@ -0,0 +1,67 @@ +"use strict"; + +const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +const TIMEOUT_PAGE_URI_HTTP = + TEST_PATH_HTTP + "file_httpsfirst_timeout_server.sjs"; + +async function runPrefTest(aURI, aDesc, aAssertURLStartsWith) { + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + const loaded = BrowserTestUtils.browserLoaded(browser, false, null, true); + BrowserTestUtils.loadURI(browser, aURI); + await loaded; + + await ContentTask.spawn(browser, { aDesc, aAssertURLStartsWith }, function({ + aDesc, + aAssertURLStartsWith, + }) { + ok( + content.document.location.href.startsWith(aAssertURLStartsWith), + aDesc + ); + }); + }); +} + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", false]], + }); + + await runPrefTest( + "http://example.com", + "HTTPS-First disabled; Should not upgrade", + "http://" + ); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", true]], + }); + + await runPrefTest( + "http://example.com", + "Should upgrade upgradeable website", + "https://" + ); + + await runPrefTest( + "http://httpsfirst.com", + "Should downgrade after error.", + "http://" + ); + + await runPrefTest( + "http://domain.does.not.exist", + "Should not downgrade on dnsNotFound error.", + "https://" + ); + + await runPrefTest( + TIMEOUT_PAGE_URI_HTTP, + "Should downgrade after timeout.", + "http://" + ); +}); diff --git a/dom/security/test/https-first/browser_httpsfirst_console_logging.js b/dom/security/test/https-first/browser_httpsfirst_console_logging.js new file mode 100644 index 0000000000..5d0ae7439d --- /dev/null +++ b/dom/security/test/https-first/browser_httpsfirst_console_logging.js @@ -0,0 +1,71 @@ +// Bug 1658924 - HTTPS-First Mode - Tests for console logging +// https://bugzilla.mozilla.org/show_bug.cgi?id=1658924 +// This test makes sure that the various console messages from the HTTPS-First +// mode get logged to the console. +"use strict"; + +// Test Cases +// description: Description of what the subtests expects. +// expectLogLevel: Expected log-level of a message. +// expectIncludes: Expected substrings the message should contain. +let tests = [ + { + description: "Top-Level upgrade should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: ["Upgrading insecure request", "to use", "httpsfirst.com"], + }, + { + description: "Top-Level upgrade failure should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: [ + "Upgrading insecure request", + "failed", + "httpsfirst.com", + "Downgrading to", + ], + }, +]; + +add_task(async function() { + // A longer timeout is necessary for this test than the plain mochitests + // due to opening a new tab with the web console. + requestLongerTimeout(4); + + // Enable HTTPS-First Mode and register console-listener + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", true]], + }); + Services.console.registerListener(on_new_message); + // 1. Upgrade page to https:// + await BrowserTestUtils.loadURI( + gBrowser.selectedBrowser, + "http://httpsfirst.com" + ); + + await BrowserTestUtils.waitForCondition(() => tests.length === 0); + + // Clean up + Services.console.unregisterListener(on_new_message); +}); + +function on_new_message(msgObj) { + const message = msgObj.message; + const logLevel = msgObj.logLevel; + + if (message.includes("HTTPS-First Mode:")) { + for (let i = 0; i < tests.length; i++) { + const testCase = tests[i]; + // Check if log-level matches + if (logLevel !== testCase.expectLogLevel) { + continue; + } + // Check if all substrings are included + if (testCase.expectIncludes.some(str => !message.includes(str))) { + continue; + } + ok(true, testCase.description); + tests.splice(i, 1); + break; + } + } +} diff --git a/dom/security/test/https-first/browser_httpsfirst_speculative_connect.js b/dom/security/test/https-first/browser_httpsfirst_speculative_connect.js new file mode 100644 index 0000000000..0d5f6c7e33 --- /dev/null +++ b/dom/security/test/https-first/browser_httpsfirst_speculative_connect.js @@ -0,0 +1,69 @@ +"use strict"; + +const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +let console_messages = [ + { + description: "Speculative Connection should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: [ + "Upgrading insecure speculative TCP connection", + "to use", + "example.com", + "file_httpsfirst_speculative_connect.html", + ], + }, + { + description: "Upgrade should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: [ + "Upgrading insecure request", + "to use", + "example.com", + "file_httpsfirst_speculative_connect.html", + ], + }, +]; + +function on_new_console_messages(msgObj) { + const message = msgObj.message; + const logLevel = msgObj.logLevel; + + if (message.includes("HTTPS-First Mode:")) { + for (let i = 0; i < console_messages.length; i++) { + const testCase = console_messages[i]; + // Check if log-level matches + if (logLevel !== testCase.expectLogLevel) { + continue; + } + // Check if all substrings are included + if (testCase.expectIncludes.some(str => !message.includes(str))) { + continue; + } + ok(true, testCase.description); + console_messages.splice(i, 1); + break; + } + } +} + +add_task(async function() { + requestLongerTimeout(4); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", true]], + }); + Services.console.registerListener(on_new_console_messages); + + await BrowserTestUtils.loadURI( + gBrowser.selectedBrowser, + `${TEST_PATH_HTTP}file_httpsfirst_speculative_connect.html` + ); + + await BrowserTestUtils.waitForCondition(() => console_messages.length === 0); + + Services.console.unregisterListener(on_new_console_messages); +}); diff --git a/dom/security/test/https-first/browser_mixed_content_console.js b/dom/security/test/https-first/browser_mixed_content_console.js new file mode 100644 index 0000000000..6663525e43 --- /dev/null +++ b/dom/security/test/https-first/browser_mixed_content_console.js @@ -0,0 +1,104 @@ +// Bug 1713593: HTTPS-First: Add test for mixed content blocker. +"use strict"; + +const testPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +const UPGRADE_DISPLAY_CONTENT = + "security.mixed_content.upgrade_display_content"; + +let threeMessagesArrived = 0; +let messageImageSeen = false; + +const kTestURI = testPath + "file_mixed_content_console.html"; + +add_task(async function() { + // A longer timeout is necessary for this test than the plain mochitests + // due to opening a new tab with the web console. + requestLongerTimeout(4); + + // Enable HTTPS-First Mode and register console-listener + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", true]], + }); + Services.console.registerListener(on_console_message); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, kTestURI); + + await BrowserTestUtils.waitForCondition(() => threeMessagesArrived === 3); + + Services.console.unregisterListener(on_console_message); +}); + +function on_console_message(msgObj) { + const message = msgObj.message; + + // The first console message is: + // "HTTPS-First Mode: Upgrading insecure request + // ‘http://example.com/browser/dom/security/test/https-first/file_mixed_content_console.html’ to use ‘https’" + if (message.includes("HTTPS-First Mode: Upgrading insecure request")) { + ok(message.includes("Upgrading insecure request"), "request got upgraded"); + ok( + message.includes( + "“http://example.com/browser/dom/security/test/https-first/file_mixed_content_console.html†to use “httpsâ€." + ), + "correct top-level request" + ); + threeMessagesArrived++; + } + // If security.mixed_content.upgrade_display_content is enabled: + // The second console message is about upgrading the insecure image + else if ( + Services.prefs.getBoolPref(UPGRADE_DISPLAY_CONTENT) && + message.includes("Mixed Content: Upgrading") + ) { + ok( + message.includes("insecure display request"), + "display content got load" + ); + ok( + message.includes( + "‘http://example.com/browser/dom/security/test/https-first/auto_upgrading_identity.png’ to use ‘https’" + ), + "img loaded secure" + ); + threeMessagesArrived++; + messageImageSeen = true; + } + // Else: + // The second console message is about blocking the image: + // Message: "Loading mixed (insecure) display content + // “http://example.com/browser/dom/security/test/https-first/auto_upgrading_identity.png†on a secure page". + // Since the message is send twice, prevent reading the image message two times + else if (message.includes("Loading mixed") && !messageImageSeen) { + ok( + message.includes("Loading mixed (insecure) display content"), + "display content got load" + ); + ok( + message.includes( + "“http://example.com/browser/dom/security/test/https-first/auto_upgrading_identity.png†on a secure page" + ), + "img loaded insecure" + ); + threeMessagesArrived++; + messageImageSeen = true; + } + // The third message is: + // "Blocked loading mixed active content + // "http://example.com/browser/dom/security/test/https-first/barfoo"" + else if (message.includes("Blocked loading")) { + ok( + message.includes("Blocked loading mixed active content"), + "script got blocked" + ); + ok( + message.includes( + "http://example.com/browser/dom/security/test/https-first/barfoo" + ), + "the right script got blocked" + ); + threeMessagesArrived++; + } +} diff --git a/dom/security/test/https-first/browser_mixed_content_download.js b/dom/security/test/https-first/browser_mixed_content_download.js new file mode 100644 index 0000000000..622c085ca8 --- /dev/null +++ b/dom/security/test/https-first/browser_mixed_content_download.js @@ -0,0 +1,157 @@ +ChromeUtils.defineESModuleGetters(this, { + Downloads: "resource://gre/modules/Downloads.sys.mjs", + DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs", +}); + +const HandlerService = Cc[ + "@mozilla.org/uriloader/handler-service;1" +].getService(Ci.nsIHandlerService); + +const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + +let SECURE_BASE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "https://example.com/" + ) + "download_page.html"; + +/** + * Waits until a download is triggered. + * It waits until a prompt is shown, + * saves and then accepts the dialog. + * @returns {Promise} Resolved once done. + */ + +function shouldTriggerDownload() { + return new Promise((resolve, reject) => { + Services.wm.addListener({ + onOpenWindow(xulWin) { + Services.wm.removeListener(this); + let win = xulWin.docShell.domWindow; + waitForFocus(() => { + if ( + win.location == + "chrome://mozapps/content/downloads/unknownContentType.xhtml" + ) { + let dialog = win.document.getElementById("unknownContentType"); + let button = dialog.getButton("accept"); + let actionRadio = win.document.getElementById("save"); + actionRadio.click(); + button.disabled = false; + dialog.acceptDialog(); + resolve(); + } else { + reject(); + } + }, win); + }, + }); + }); +} + +const CONSOLE_UPGRADE_MESSAGE = "Upgrading insecure request"; +const CONSOLE_DOWNGRADE_MESSAGE = "Downgrading to “http†again."; +const DOWNLOAD_URL = + "example.com/browser/dom/security/test/https-first/download_server.sjs"; +// Verifies that https-first tries to upgrade download, +// falls back since download is not available via https +let msgCounter = 0; +function shouldConsoleError() { + return new Promise((resolve, reject) => { + function listener(msgObj) { + let text = msgObj.message; + if (text.includes(CONSOLE_UPGRADE_MESSAGE) && msgCounter == 0) { + ok( + text.includes("http://" + DOWNLOAD_URL), + "Https-first tries to upgrade download to https" + ); + msgCounter++; + } + if (text.includes(CONSOLE_DOWNGRADE_MESSAGE) && msgCounter == 1) { + ok( + text.includes("https://" + DOWNLOAD_URL), + "Https-first downgrades download to http." + ); + resolve(); + Services.console.unregisterListener(listener); + } + } + Services.console.registerListener(listener); + }); +} + +async function resetDownloads() { + // Removes all downloads from the download List + const types = new Set(); + let publicList = await Downloads.getList(Downloads.PUBLIC); + let downloads = await publicList.getAll(); + for (let download of downloads) { + if (download.contentType) { + types.add(download.contentType); + } + publicList.remove(download); + await download.finalize(true); + } + + if (types.size) { + // reset handlers for the contentTypes of any files previously downloaded + for (let type of types) { + const mimeInfo = MIMEService.getFromTypeAndExtension(type, ""); + info("resetting handler for type: " + type); + HandlerService.remove(mimeInfo); + } + } +} + +async function runTest(url, link, checkFunction, description) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_first", true], + ["browser.download.always_ask_before_handling_new_types", true], + ["browser.download.improvements_to_download_panel", false], + ], + }); + requestLongerTimeout(2); + await resetDownloads(); + + let tab = BrowserTestUtils.addTab(gBrowser, url); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + is( + gBrowser.currentURI.schemeIs("https"), + true, + "Scheme of opened tab should be https" + ); + info("Checking: " + description); + + let checkPromise = checkFunction(); + // Click the Link to trigger the download + SpecialPowers.spawn(gBrowser.selectedBrowser, [link], contentLink => { + content.document.getElementById(contentLink).click(); + }); + await checkPromise; + ok(true, description); + BrowserTestUtils.removeTab(tab); +} + +//Test description: +// 1. Open "https://example.com" +// 2. From "https://example.com" download something, but that download is only available via http. +// 3. Https-first tries to upgrade the download. +// 4. Upgrading fails - so http-first downgrade download to http. + +add_task(async function test_mixed_download() { + await runTest( + SECURE_BASE_URL, + "insecure", + () => Promise.all([shouldTriggerDownload(), shouldConsoleError()]), + "Secure -> Insecure should Error" + ); + // remove downloaded file + let downloadsPromise = Downloads.getList(Downloads.PUBLIC); + let downloadList = await downloadsPromise; + let [download] = downloadList._downloads; + await downloadList.remove(download); +}); diff --git a/dom/security/test/https-first/browser_navigation.js b/dom/security/test/https-first/browser_navigation.js new file mode 100644 index 0000000000..9f2c3b9128 --- /dev/null +++ b/dom/security/test/https-first/browser_navigation.js @@ -0,0 +1,97 @@ +"use strict"; + +const REQUEST_URL = + "http://httpsfirst.com/browser/dom/security/test/https-first/"; + +async function promiseGetHistoryIndex(browser) { + if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { + return SpecialPowers.spawn(browser, [], function() { + let shistory = + docShell.browsingContext.childSessionHistory.legacySHistory; + return shistory.index; + }); + } + + let shistory = browser.browsingContext.sessionHistory; + return shistory.index; +} + +async function testNavigations() { + // Load initial site + + let url1 = REQUEST_URL + "file_navigation.html?foo1"; + let url2 = REQUEST_URL + "file_navigation.html?foo2"; + let url3 = REQUEST_URL + "file_navigation.html?foo3"; + + let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + BrowserTestUtils.loadURI(gBrowser, url1); + await loaded; + + // Load another site + loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [url2], + async url => (content.location.href = url) + ); + await loaded; + + // Load another site + loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [url3], + async url => (content.location.href = url) + ); + await loaded; + is( + await promiseGetHistoryIndex(gBrowser.selectedBrowser), + 2, + "correct session history index after load 3" + ); + + // Go back one site by clicking the back button + info("Clicking back button"); + loaded = BrowserTestUtils.waitForLocationChange(gBrowser, url2); + let backButton = document.getElementById("back-button"); + backButton.click(); + await loaded; + is( + await promiseGetHistoryIndex(gBrowser.selectedBrowser), + 1, + "correct session history index after going back for the first time" + ); + + // Go back again + info("Clicking back button again"); + loaded = BrowserTestUtils.waitForLocationChange(gBrowser, url1); + backButton.click(); + await loaded; + is( + await promiseGetHistoryIndex(gBrowser.selectedBrowser), + 0, + "correct session history index after going back for the second time" + ); +} + +add_task(async function() { + waitForExplicitFinish(); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", true]], + }); + + await testNavigations(); + + // If fission is enabled we also want to test the navigations with the bfcache + // in the parent. + if (SpecialPowers.getBoolPref("fission.autostart")) { + await SpecialPowers.pushPrefEnv({ + set: [["fission.bfcacheInParent", true]], + }); + + await testNavigations(); + } + + finish(); +}); diff --git a/dom/security/test/https-first/browser_slow_download.js b/dom/security/test/https-first/browser_slow_download.js new file mode 100644 index 0000000000..8766e12f34 --- /dev/null +++ b/dom/security/test/https-first/browser_slow_download.js @@ -0,0 +1,162 @@ +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", +}); +// Create a uri for an https site +const testPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const TEST_URI = testPath + "file_slow_download.html"; +const EXPECTED_DOWNLOAD_URL = + "example.com/browser/dom/security/test/https-first/file_slow_download.sjs"; + +// Since the server send the complete download file after 3 seconds we need an longer timeout +requestLongerTimeout(4); + +function promisePanelOpened() { + if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") { + return Promise.resolve(); + } + return BrowserTestUtils.waitForEvent(DownloadsPanel.panel, "popupshown"); +} + +/** + * Waits for a download to finish, in case it has not finished already. + * + * @param aDownload + * The Download object to wait upon. + * + * @return {Promise} + * @resolves When the download has finished successfully. + * @rejects JavaScript exception if the download failed. + */ +function promiseDownloadStopped(aDownload) { + if (!aDownload.stopped) { + // The download is in progress, wait for the current attempt to finish and + // report any errors that may occur. + return aDownload.start(); + } + + if (aDownload.succeeded) { + return Promise.resolve(); + } + + // The download failed or was canceled. + return Promise.reject(aDownload.error || new Error("Download canceled.")); +} + +// Verifys that no background request was send +let requestCounter = 0; +function examiner() { + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} + +examiner.prototype = { + observe(subject, topic, data) { + if (topic !== "specialpowers-http-notify-request") { + return; + } + // On Android we have other requests appear here as well. Let's make + // sure we only evaluate requests triggered by the test. + if ( + !data.startsWith("http://example.com") && + !data.startsWith("https://example.com") + ) { + return; + } + ++requestCounter; + if (requestCounter == 1) { + is(data, TEST_URI, "Download start page is https"); + return; + } + if (requestCounter == 2) { + // The specialpowers-http-notify-request fires before the internal redirect( /upgrade) to + // https happens. + is( + data, + "http://" + EXPECTED_DOWNLOAD_URL, + "First download request is http (internal)" + ); + return; + } + if (requestCounter == 3) { + is( + data, + "https://" + EXPECTED_DOWNLOAD_URL, + "Download got upgraded to https" + ); + return; + } + ok(false, "we should never get here, but just in case"); + }, + remove() { + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + }, +}; + +// Test description: +// 1. Open https://example.com +// 2. Start download - location of download is http +// 3. https-first upgrades to https +// 4. Server send first part of download and after 3 seconds the rest +// 5. Complete download of text file +add_task(async function test_slow_download() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_first", true], + // ensure that download panel gets opened + ["browser.download.improvements_to_download_panel", true], + ], + }); + + // remove all previous downloads + let downloadsList = await Downloads.getList(Downloads.PUBLIC); + await downloadsList.removeFinished(); + + // add observer to ensure that the background request gets canceled for the upgraded Download + this.examiner = new examiner(); + + let downloadsPanelPromise = promisePanelOpened(); + let downloadsPromise = Downloads.getList(Downloads.PUBLIC); + BrowserTestUtils.loadURI(gBrowser, TEST_URI); + // wait for downloadsPanel to open before continuing with test + await downloadsPanelPromise; + let downloadList = await downloadsPromise; + is(DownloadsPanel.isPanelShowing, true, "DownloadsPanel should be open."); + is(downloadList._downloads.length, 1, "File should be downloaded."); + let [download] = downloadList._downloads; + // wait for download to finish (with success or error) + await promiseDownloadStopped(download); + is(download.contentType, "text/plain", "File contentType should be correct."); + // ensure https-first did upgrade the scheme. + is( + download.source.url, + "https://" + EXPECTED_DOWNLOAD_URL, + "Scheme should be https." + ); + // ensure that no background request was send + is( + requestCounter, + 3, + "three requests total (download page, download http, download https/ upgraded)" + ); + // ensure that downloaded is complete + is(download.target.size, 25, "Download size is correct"); + //clean up + this.examiner.remove(); + info("cleaning up downloads"); + try { + if (Services.appinfo.OS === "WINNT") { + // We need to make the file writable to delete it on Windows. + await IOUtils.setPermissions(download.target.path, 0o600); + } + await IOUtils.remove(download.target.path); + } catch (error) { + info("The file " + download.target.path + " is not removed, " + error); + } + + await downloadList.remove(download); + await download.finalize(); +}); diff --git a/dom/security/test/https-first/browser_upgrade_onion.js b/dom/security/test/https-first/browser_upgrade_onion.js new file mode 100644 index 0000000000..c8ab29bd1c --- /dev/null +++ b/dom/security/test/https-first/browser_upgrade_onion.js @@ -0,0 +1,59 @@ +// This test ensures that various configurable upgrade exceptions work +"use strict"; + +async function runTest(desc, url, expectedURI) { + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + let loaded = BrowserTestUtils.browserLoaded(browser, false, null, true); + BrowserTestUtils.loadURI(browser, url); + await loaded; + + await SpecialPowers.spawn(browser, [desc, expectedURI], async function( + desc, + expectedURI + ) { + // XXX ckerschb: generally we use the documentURI, but our test infra + // can not handle .onion, hence we use the URI of the failed channel + // stored on the docshell to see if the scheme was upgraded to https. + let loadedURI = content.document.documentURI; + if (loadedURI.startsWith("about:neterror")) { + loadedURI = content.docShell.failedChannel.URI.spec; + } + is(loadedURI, expectedURI, desc); + }); + }); +} + +// by default local addresses and .onion should *not* get upgraded +add_task(async function() { + requestLongerTimeout(2); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_first", true], + ["dom.security.https_only_mode", false], + ["dom.security.https_only_mode.upgrade_local", false], + ["dom.security.https_only_mode.upgrade_onion", false], + ], + }); + + await runTest( + "Hosts ending with .onion should be be exempt from HTTPS-First upgrades by default", + "http://grocery.shopping.for.one.onion/", + "http://grocery.shopping.for.one.onion/" + ); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_first", true], + ["dom.security.https_only_mode", false], + ["dom.security.https_only_mode.upgrade_local", false], + ["dom.security.https_only_mode.upgrade_onion", true], + ], + }); + + await runTest( + "Hosts ending with .onion should get upgraded when 'dom.security.https_only_mode.upgrade_onion' is set to true", + "http://grocery.shopping.for.one.onion/", + "https://grocery.shopping.for.one.onion/" + ); +}); diff --git a/dom/security/test/https-first/download_page.html b/dom/security/test/https-first/download_page.html new file mode 100644 index 0000000000..a828ee07db --- /dev/null +++ b/dom/security/test/https-first/download_page.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test mixed content download by https-first</title> + </head> + <body> + hi + + <script> + const host = window.location.host; + const path = location.pathname.replace("download_page.html","download_server.sjs"); + + const insecureLink = document.createElement("a"); + insecureLink.href=`http://${host}${path}`; + insecureLink.download="true"; + insecureLink.id="insecure"; + insecureLink.textContent="Not secure Link"; + + document.body.append(insecureLink); + </script> + </body> +</html> diff --git a/dom/security/test/https-first/download_server.sjs b/dom/security/test/https-first/download_server.sjs new file mode 100644 index 0000000000..7af5722e7b --- /dev/null +++ b/dom/security/test/https-first/download_server.sjs @@ -0,0 +1,16 @@ +function handleRequest(request, response) { + // Only answer to http, in case request is in https let the reply time out. + if (request.scheme === "https") { + response.processAsync(); + return; + } + + response.setHeader("Cache-Control", "no-cache", false); + // Send some file, e.g. an image + response.setHeader( + "Content-Disposition", + "attachment; filename=some-file.png" + ); + response.setHeader("Content-Type", "image/png"); + response.write("Success!"); +} diff --git a/dom/security/test/https-first/file_bad_cert.sjs b/dom/security/test/https-first/file_bad_cert.sjs new file mode 100644 index 0000000000..1a8ae08a86 --- /dev/null +++ b/dom/security/test/https-first/file_bad_cert.sjs @@ -0,0 +1,34 @@ +const RESPONSE_SUCCESS = ` + <html> + <body> + send message, downgraded + <script type="application/javascript"> + let scheme = document.location.protocol; + window.opener.postMessage({result: 'downgraded', scheme: scheme}, '*'); + </script> + </body> + </html>`; + +const RESPONSE_UNEXPECTED = ` + <html> + <body> + send message, error + <script type="application/javascript"> + let scheme = document.location.protocol; + window.opener.postMessage({result: 'Error', scheme: scheme}, '*'); + </script> + </body> + </html>`; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + // if the received request is not http send an error + if (request.scheme === "http") { + response.write(RESPONSE_SUCCESS); + return; + } + // we should never get here; just in case, return something unexpected + response.write(RESPONSE_UNEXPECTED); +} diff --git a/dom/security/test/https-first/file_beforeunload_permit_http.html b/dom/security/test/https-first/file_beforeunload_permit_http.html new file mode 100644 index 0000000000..50459d6006 --- /dev/null +++ b/dom/security/test/https-first/file_beforeunload_permit_http.html @@ -0,0 +1,9 @@ +<!DOCTYPE html><html> +<body> + <script> + window.onbeforeunload = function() { + return "stop"; + } + </script> +</body> +</html> diff --git a/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs b/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs new file mode 100644 index 0000000000..eb64c59e97 --- /dev/null +++ b/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs @@ -0,0 +1,61 @@ +"use strict"; + +const REDIRECT_URI = + "http://example.com/tests/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs?verify"; +const DOWNGRADE_URI = + "http://example.com/tests/dom/security/test/https-first/file_downgrade_with_different_path.sjs"; +const RESPONSE_ERROR = "unexpected-query"; + +// An onload postmessage to window opener +const RESPONSE_HTTPS_SCHEME = ` + <html> + <body> + <script type="application/javascript"> + window.opener.postMessage({result: 'scheme-https'}, '*'); + </script> + </body> + </html>`; + +const RESPONSE_HTTP_SCHEME = ` + <html> + <body> + <script type="application/javascript"> + window.opener.postMessage({result: 'scheme-http'}, '*'); + </script> + </body> + </html>`; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + const query = request.queryString; + + if (query == "downgrade") { + // send same-origin downgrade from https: to http: with a different path. + // we don't consider it's an endless upgrade downgrade loop in this case. + response.setStatusLine(request.httpVersion, 302, "Found"); + response.setHeader("Location", DOWNGRADE_URI, false); + return; + } + + // handle the redirect case + if ((query >= 301 && query <= 303) || query == 307) { + // send same-origin downgrade from https: to http: again simluating + // and endless upgrade downgrade loop. + response.setStatusLine(request.httpVersion, query, "Found"); + response.setHeader("Location", REDIRECT_URI, false); + return; + } + + // Check if scheme is http:// or https:// + if (query == "verify") { + let response_content = + request.scheme === "https" ? RESPONSE_HTTPS_SCHEME : RESPONSE_HTTP_SCHEME; + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(response_content); + return; + } + + // We should never get here, but just in case ... + response.setStatusLine(request.httpVersion, 500, "OK"); + response.write("unexepcted query"); +} diff --git a/dom/security/test/https-first/file_data_uri.html b/dom/security/test/https-first/file_data_uri.html new file mode 100644 index 0000000000..69133e5079 --- /dev/null +++ b/dom/security/test/https-first/file_data_uri.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1709069: Test that Data URI which makes a top-level request gets updated in https-first</title> +</head> +<body> +<script class="testbody" type="text/javascript"> + window.onload = (event) => { + let myLoc = window.location.href; + window.opener.parent.postMessage({location: myLoc}, "*"); + window.close(); +}; +</script> +</body> +</html> diff --git a/dom/security/test/https-first/file_downgrade_500_responses.sjs b/dom/security/test/https-first/file_downgrade_500_responses.sjs new file mode 100644 index 0000000000..b3cfbd79dd --- /dev/null +++ b/dom/security/test/https-first/file_downgrade_500_responses.sjs @@ -0,0 +1,70 @@ +// Custom *.sjs file specifically for the needs of Bug 1709552 +"use strict"; + +const RESPONSE_SUCCESS = ` + <html> + <body> + send message, downgraded + <script type="application/javascript"> + let scheme = document.location.protocol; + window.opener.postMessage({result: 'downgraded', scheme: scheme}, '*'); + </script> + </body> + </html>`; + +const RESPONSE_UNEXPECTED = ` + <html> + <body> + send message, error + <script type="application/javascript"> + let scheme = document.location.protocol; + window.opener.postMessage({result: 'Error', scheme: scheme}, '*'); + </script> + </body> + </html>`; + +function handleRequest(request, response) { + // avoid confusing cache behaviour + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + let query = request.queryString; + // if the scheme is not https and it is the initial request + // then we rather fall through and display unexpected content + if (request.scheme === "https") { + if (query === "test1a") { + response.setStatusLine("1.1", 501, "Not Implemented"); + response.write("Not Implemented\n"); + return; + } + + if (query === "test2a") { + response.setStatusLine("1.1", 504, "Gateway Timeout"); + response.write("Gateway Timeout\n"); + return; + } + + if (query === "test3a") { + response.setStatusLine("1.1", 521, "Web Server Is Down"); + response.write("Web Server Is Down\n"); + return; + } + if (query === "test4a") { + response.setStatusLine("1.1", 530, "Railgun Error"); + response.write("Railgun Error\n"); + return; + } + if (query === "test5a") { + response.setStatusLine("1.1", 560, "Unauthorized"); + response.write("Unauthorized\n"); + return; + } + + // We should never arrive here, just in case send something unexpected + response.write(RESPONSE_UNEXPECTED); + return; + } + + // We should arrive here when the redirection was downraded successful + response.write(RESPONSE_SUCCESS); +} diff --git a/dom/security/test/https-first/file_downgrade_bad_responses.sjs b/dom/security/test/https-first/file_downgrade_bad_responses.sjs new file mode 100644 index 0000000000..1a6eea2dd1 --- /dev/null +++ b/dom/security/test/https-first/file_downgrade_bad_responses.sjs @@ -0,0 +1,73 @@ +// Custom *.sjs file specifically for the needs of Bug 1709552 +"use strict"; + +const RESPONSE_SUCCESS = ` + <html> + <body> + send message, downgraded + <script type="application/javascript"> + window.opener.postMessage({result: 'downgraded', scheme: 'http'}, '*'); + </script> + </body> + </html>`; + +const RESPONSE_UNEXPECTED = ` + <html> + <body> + send message, error + <script type="application/javascript"> + window.opener.postMessage({result: 'Error', scheme: 'http'}, '*'); + </script> + </body> + </html>`; + +function handleRequest(request, response) { + // avoid confusing cache behaviour + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + let query = request.queryString; + // if the scheme is not https and it is the initial request + // then we rather fall through and display unexpected content + if (request.scheme === "https") { + if (query === "test1a") { + response.setStatusLine("1.1", 400, "Bad Request"); + response.write("Bad Request\n"); + return; + } + + if (query === "test2a") { + response.setStatusLine("1.1", 403, "Forbidden"); + response.write("Forbidden\n"); + return; + } + + if (query === "test3a") { + response.setStatusLine("1.1", 404, "Not Found"); + response.write("Not Found\n"); + return; + } + if (query === "test4a") { + response.setStatusLine("1.1", 416, "Requested Range Not Satisfiable"); + response.write("Requested Range Not Satisfiable\n"); + return; + } + if (query === "test5a") { + response.setStatusLine("1.1", 418, "I'm a teapot"); + response.write("I'm a teapot\n"); + return; + } + if (query == "test6a") { + // Simulating a timeout by processing the https request + response.processAsync(); + return; + } + + // We should never arrive here, just in case send something unexpected + response.write(RESPONSE_UNEXPECTED); + return; + } + + // We should arrive here when the redirection was downraded successful + response.write(RESPONSE_SUCCESS); +} diff --git a/dom/security/test/https-first/file_downgrade_request_upgrade_request.sjs b/dom/security/test/https-first/file_downgrade_request_upgrade_request.sjs new file mode 100644 index 0000000000..6004d57eaf --- /dev/null +++ b/dom/security/test/https-first/file_downgrade_request_upgrade_request.sjs @@ -0,0 +1,52 @@ +// Custom *.sjs file specifically for the needs of Bug 1706126 +"use strict"; +// subdomain of example.com +const REDIRECT_302 = + "http://www.redirect-example.com/tests/dom/security/test/https-first/file_downgrade_request_upgrade_request.sjs"; + +const RESPONSE_SUCCESS = ` + <html> + <body> + send message, upgraded + <script type="application/javascript"> + let scheme = document.location.protocol; + window.opener.postMessage({result: 'upgraded', scheme: scheme}, '*'); + </script> + </body> + </html>`; + +const RESPONSE_UNEXPECTED = ` + <html> + <body> + send message, error + <script type="application/javascript"> + let scheme = document.location.protocol; + window.opener.postMessage({result: 'error', scheme: scheme}, '*'); + </script> + </body> + </html>`; + +function handleRequest(request, response) { + // avoid confusing cache behaviour + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + // if the scheme is https and it is the initial request time it out + if (request.scheme === "https" && request.host === "redirect-example.com") { + // Simulating a timeout by processing the https request + response.processAsync(); + return; + } + if (request.scheme === "http" && request.host === "redirect-example.com") { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", REDIRECT_302, false); + return; + } + // if the request was sent to subdomain + if (request.host.startsWith("www.")) { + response.write(RESPONSE_SUCCESS); + return; + } + // We should never arrive here, just in case send 'error' + response.write(RESPONSE_UNEXPECTED); +} diff --git a/dom/security/test/https-first/file_downgrade_view_source.sjs b/dom/security/test/https-first/file_downgrade_view_source.sjs new file mode 100644 index 0000000000..44fe4d99d3 --- /dev/null +++ b/dom/security/test/https-first/file_downgrade_view_source.sjs @@ -0,0 +1,30 @@ +"use strict"; + +function handleRequest(request, response) { + // avoid confusing cache behaviour + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + let query = request.queryString; + let scheme = request.scheme; + + if (scheme === "https") { + if (query === "downgrade") { + response.setStatusLine("1.1", 400, "Bad Request"); + response.write("Bad Request\n"); + return; + } + if (query === "upgrade") { + response.write("view-source:https://"); + return; + } + } + + if (scheme === "http" && query === "downgrade") { + response.write("view-source:http://"); + return; + } + + // We should arrive here when the redirection was downraded successful + response.write("unexpected"); +} diff --git a/dom/security/test/https-first/file_downgrade_with_different_path.sjs b/dom/security/test/https-first/file_downgrade_with_different_path.sjs new file mode 100644 index 0000000000..7450313d98 --- /dev/null +++ b/dom/security/test/https-first/file_downgrade_with_different_path.sjs @@ -0,0 +1,27 @@ +"use strict"; + +// An onload postmessage to window opener +const RESPONSE_HTTPS_SCHEME = ` + <html> + <body> + <script type="application/javascript"> + window.opener.postMessage({result: 'scheme-https'}, '*'); + </script> + </body> + </html>`; + +const RESPONSE_HTTP_SCHEME = ` + <html> + <body> + <script type="application/javascript"> + window.opener.postMessage({result: 'scheme-http'}, '*'); + </script> + </body> + </html>`; + +function handleRequest(request, response) { + let response_content = + request.scheme === "https" ? RESPONSE_HTTPS_SCHEME : RESPONSE_HTTP_SCHEME; + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(response_content); +} diff --git a/dom/security/test/https-first/file_download_attribute.html b/dom/security/test/https-first/file_download_attribute.html new file mode 100644 index 0000000000..453bf408b3 --- /dev/null +++ b/dom/security/test/https-first/file_download_attribute.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test download attribute for http site</title> +</head> +<body> + <a href="http://nocert.example.com/browser/dom/security/test/https-first/file_download_attribute.sjs" download="some.html" id="testlink">download by attribute</a> + <script> + // click the link to start download + let testlink = document.getElementById("testlink"); + testlink.click(); + </script> + </body> +</html> diff --git a/dom/security/test/https-first/file_download_attribute.sjs b/dom/security/test/https-first/file_download_attribute.sjs new file mode 100644 index 0000000000..8941da1a41 --- /dev/null +++ b/dom/security/test/https-first/file_download_attribute.sjs @@ -0,0 +1,12 @@ +function handleRequest(request, response) { + // Only answer to http, in case request is in https let the reply time out. + if (request.scheme === "https") { + response.processAsync(); + return; + } + + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Disposition", "attachment; filename=some.html"); + response.setHeader("Content-Type", "text/html"); + response.write("success!"); +} diff --git a/dom/security/test/https-first/file_form_submission.sjs b/dom/security/test/https-first/file_form_submission.sjs new file mode 100644 index 0000000000..63b248d773 --- /dev/null +++ b/dom/security/test/https-first/file_form_submission.sjs @@ -0,0 +1,84 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +const RESPONSE_SUCCESS = ` + <html> + <body> + send message, downgraded + <script type="application/javascript"> + let scheme = document.location.protocol; + const loc = document.location.href; + window.opener.postMessage({location: loc, scheme: scheme, form:"test=success" }, '*'); + </script> + </body> + </html>`; + +const POST_FORMULAR = ` +<html> + <body> + <form action="http://example.com/tests/dom/security/test/https-first/file_form_submission.sjs?" method="POST" id="POSTForm"> + <div> + <label id="submit">Submit</label> + <input name="test" id="form" value="success"> + </div> + </form> + <script class="testbody" type="text/javascript"> + document.getElementById("POSTForm").submit(); + </script> + </body> +</html> +`; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + let queryString = request.queryString; + if (request.scheme === "https" && queryString === "test=1") { + response.write(RESPONSE_SUCCESS); + return; + } + if ( + request.scheme === "https" && + (queryString === "test=2" || queryString === "test=4") + ) { + // time out request + response.processAsync(); + return; + } + if (request.scheme === "http" && queryString === "test=2") { + response.write(RESPONSE_SUCCESS); + return; + } + if (queryString === "test=3" || queryString === "test=4") { + // send post form + response.write(POST_FORMULAR); + return; + } + if (request.method == "POST") { + // extract form parameters + let body = new BinaryInputStream(request.bodyInputStream); + let avail; + let bytes = []; + while ((avail = body.available()) > 0) { + Array.prototype.push.apply(bytes, body.readByteArray(avail)); + } + let requestBodyContents = String.fromCharCode.apply(null, bytes); + + response.write(` + <html> + <script type="application/javascript"> + let scheme = document.location.protocol; + const loc = document.location.href; + window.opener.postMessage({location: loc, scheme: scheme, form: '${requestBodyContents}'}, '*'); + </script> + </html>`); + + return; + } + // we should never get here; just in case, return something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/https-first/file_fragment.html b/dom/security/test/https-first/file_fragment.html new file mode 100644 index 0000000000..4d042ebc15 --- /dev/null +++ b/dom/security/test/https-first/file_fragment.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<script> + +function beforeunload(){ + window.opener.postMessage({ + info: "before-unload", + result: window.location.hash, + button: false, + }, "*"); +} + +window.onload = function (){ + let button = window.document.getElementById("clickMeButton"); + let buttonExist = button !== null; + window.opener.postMessage({ + info: "onload", + result: window.location.href, + button: buttonExist, + }, "*"); + button.click(); + +} + +// after button clicked and paged scrolled sends URL of current window +window.onscroll = function(){ + window.opener.postMessage({ + info: "scrolled-to-foo", + result: window.location.hash, + button: true, + }, "*"); + } + + +</script> +<body onbeforeunload="/*just to notify if we load a new page*/ beforeunload()";> + <a id="clickMeButton" href="http://example.com/tests/dom/security/test/https-first/file_fragment.html#foo">Click me</a> + <div style="height: 1000px; border: 1px solid black;"> space</div> + <a name="foo" href="http://example.com/tests/dom/security/test/https-first/file_fragment.html">foo</a> + <div style="height: 1000px; border: 1px solid black;">space</div> +</body> +</html> diff --git a/dom/security/test/https-first/file_httpsfirst_speculative_connect.html b/dom/security/test/https-first/file_httpsfirst_speculative_connect.html new file mode 100644 index 0000000000..6542884191 --- /dev/null +++ b/dom/security/test/https-first/file_httpsfirst_speculative_connect.html @@ -0,0 +1 @@ +<html><body>dummy file for speculative https-first upgrade test</body></html> diff --git a/dom/security/test/https-first/file_httpsfirst_timeout_server.sjs b/dom/security/test/https-first/file_httpsfirst_timeout_server.sjs new file mode 100644 index 0000000000..81c4c0328b --- /dev/null +++ b/dom/security/test/https-first/file_httpsfirst_timeout_server.sjs @@ -0,0 +1,13 @@ +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if (request.scheme === "https") { + // Simulating a timeout by processing the https request + // async and *never* return anything! + response.processAsync(); + return; + } + // we should never get here; just in case, return something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/https-first/file_mixed_content_auto_upgrade.html b/dom/security/test/https-first/file_mixed_content_auto_upgrade.html new file mode 100644 index 0000000000..7dda8909a5 --- /dev/null +++ b/dom/security/test/https-first/file_mixed_content_auto_upgrade.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1673574 - Improve Console logging for mixed content auto upgrading</title> +</head> +<body> + <!--upgradeable resources---> + <img src="http://example.com/browser/dom/security/test/https-first/pass.png"> + <video src="http://example.com/browser/dom/security/test/https-first/test.ogv"> + <audio src="http://example.com/browser/dom/security/test/https-first/test.wav"> +</body> +</html> diff --git a/dom/security/test/https-first/file_mixed_content_console.html b/dom/security/test/https-first/file_mixed_content_console.html new file mode 100644 index 0000000000..631ac0b40f --- /dev/null +++ b/dom/security/test/https-first/file_mixed_content_console.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1713593: HTTPS-First: Add test for mixed content blocker.</title> +</head> +<body> + <!-- Test that image gets loaded (insecure) - the *.png does not actually exist because we only care for the console message to appear--> + <img type="image/png" id="testimage" src="http://example.com/browser/dom/security/test/https-first/auto_upgrading_identity.png" /> + <!-- Test that the script gets blocked. The script does not actually exist because we only care for the console message to appear--> + <script type="text/javascript" src="http://example.com/browser/dom/security/test/https-first/barfoo" > +</script> +</body> +</html> diff --git a/dom/security/test/https-first/file_multiple_redirection.sjs b/dom/security/test/https-first/file_multiple_redirection.sjs new file mode 100644 index 0000000000..49098ccdb7 --- /dev/null +++ b/dom/security/test/https-first/file_multiple_redirection.sjs @@ -0,0 +1,87 @@ +"use strict"; + +// redirection uri +const REDIRECT_URI = + "https://example.com/tests/dom/security/test/https-first/file_multiple_redirection.sjs?redirect"; +const REDIRECT_URI_HTTP = + "http://example.com/tests/dom/security/test/https-first/file_multiple_redirection.sjs?verify"; +const REDIRECT_URI_HTTPS = + "https://example.com/tests/dom/security/test/https-first/file_multiple_redirection.sjs?verify"; + +const RESPONSE_ERROR = "unexpected-query"; + +// An onload postmessage to window opener +const RESPONSE_HTTPS_SCHEME = ` + <html> + <body> + <script type="application/javascript"> + window.opener.postMessage({result: 'scheme-https'}, '*'); + </script> + </body> + </html>`; + +const RESPONSE_HTTP_SCHEME = ` + <html> + <body> + <script type="application/javascript"> + window.opener.postMessage({result: 'scheme-http'}, '*'); + </script> + </body> + </html>`; + +function sendRedirection(query, response) { + // send a redirection to an http uri + if (query.includes("test1")) { + response.setHeader("Location", REDIRECT_URI_HTTP, false); + return; + } + // send a redirection to an https uri + if (query.includes("test2")) { + response.setHeader("Location", REDIRECT_URI_HTTPS, false); + return; + } + // send a redirection to an http uri with hsts header + if (query.includes("test3")) { + response.setHeader("Strict-Transport-Security", "max-age=60"); + response.setHeader("Location", REDIRECT_URI_HTTP, false); + } +} + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + const query = request.queryString; + + // if the query contains a test query start first test + if (query.startsWith("test")) { + // send a 302 redirection + response.setStatusLine(request.httpVersion, 302, "Found"); + response.setHeader("Location", REDIRECT_URI + query, false); + return; + } + // Send a redirection + if (query.includes("redirect")) { + response.setStatusLine(request.httpVersion, 302, "Found"); + sendRedirection(query, response); + return; + } + // Reset the HSTS policy, prevent influencing other tests + if (request.queryString === "reset") { + response.setHeader("Strict-Transport-Security", "max-age=0"); + let response_content = + request.scheme === "https" ? RESPONSE_HTTPS_SCHEME : RESPONSE_HTTP_SCHEME; + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(response_content); + } + // Check if scheme is http:// or https:// + if (query == "verify") { + let response_content = + request.scheme === "https" ? RESPONSE_HTTPS_SCHEME : RESPONSE_HTTP_SCHEME; + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(response_content); + return; + } + + // We should never get here, but just in case ... + response.setStatusLine(request.httpVersion, 500, "OK"); + response.write("unexepcted query"); +} diff --git a/dom/security/test/https-first/file_navigation.html b/dom/security/test/https-first/file_navigation.html new file mode 100644 index 0000000000..02d366291b --- /dev/null +++ b/dom/security/test/https-first/file_navigation.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> + <body> + <p>Blank page</p> + <body> +</html> diff --git a/dom/security/test/https-first/file_redirect.sjs b/dom/security/test/https-first/file_redirect.sjs new file mode 100644 index 0000000000..2042bcbc88 --- /dev/null +++ b/dom/security/test/https-first/file_redirect.sjs @@ -0,0 +1,58 @@ +//https://bugzilla.mozilla.org/show_bug.cgi?id=1706351 + +// Step 1. Send request with redirect queryString (eg. file_redirect.sjs?302) +// Step 2. Server responds with corresponding redirect code to http://example.com/../file_redirect.sjs?check +// Step 3. Response from ?check indicates whether the redirected request was secure or not. + +const RESPONSE_ERROR = "unexpected-query"; + +// An onload postmessage to window opener +const RESPONSE_SECURE = ` + <html> + <body> + send onload message... + <script type="application/javascript"> + window.opener.postMessage({result: 'secure'}, '*'); + </script> + </body> + </html>`; + +const RESPONSE_INSECURE = ` + <html> + <body> + send onload message... + <script type="application/javascript"> + window.opener.postMessage({result: 'insecure'}, '*'); + </script> + </body> + </html>`; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + const query = request.queryString; + + // Send redirect header + if ((query >= 301 && query <= 303) || query == 307) { + // needs to be a cross site redirect to http://example.com otherwise + // our upgrade downgrade endless loop break mechanism kicks in + const loc = + "http://test1.example.com/tests/dom/security/test/https-first/file_redirect.sjs?check"; + response.setStatusLine(request.httpVersion, query, "Found"); + response.setHeader("Location", loc, false); + return; + } + + // Check if scheme is http:// or https:// + if (query == "check") { + const secure = + request.scheme == "https" ? RESPONSE_SECURE : RESPONSE_INSECURE; + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(secure); + return; + } + + // This should not happen + response.setStatusLine(request.httpVersion, 500, "OK"); + response.write(RESPONSE_ERROR); +} diff --git a/dom/security/test/https-first/file_redirect_downgrade.sjs b/dom/security/test/https-first/file_redirect_downgrade.sjs new file mode 100644 index 0000000000..a31c8cb99b --- /dev/null +++ b/dom/security/test/https-first/file_redirect_downgrade.sjs @@ -0,0 +1,87 @@ +// Custom *.sjs file specifically for the needs of Bug 1707856 +"use strict"; + +const REDIRECT_META = ` + <html> + <head> + <meta http-equiv="refresh" content="0; url='http://example.com/tests/dom/security/test/https-first/file_redirect_downgrade.sjs?testEnd'"> + </head> + <body> + META REDIRECT + </body> + </html>`; + +const REDIRECT_JS = ` + <html> + <body> + JS REDIRECT + <script> + let url= "http://example.com/tests/dom/security/test/https-first/file_redirect_downgrade.sjs?testEnd"; + window.location = url; + </script> + </body> + </html>`; + +const REDIRECT_302 = + "http://example.com/tests/dom/security/test/https-first/file_redirect_downgrade.sjs?testEnd"; + +const RESPONSE_SUCCESS = ` + <html> + <body> + send message, downgraded + <script type="application/javascript"> + let scheme = document.location.protocol; + window.opener.postMessage({result: 'downgraded', scheme: scheme}, '*'); + </script> + </body> + </html>`; + +const RESPONSE_UNEXPECTED = ` + <html> + <body> + send message, error + <script type="application/javascript"> + let scheme = document.location.protocol; + window.opener.postMessage({result: 'error', scheme: scheme}, '*'); + </script> + </body> + </html>`; + +function handleRequest(request, response) { + // avoid confusing cache behaviour + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + let query = request.queryString; + // if the scheme is not https and it is the initial request + // then we rather fall through and display unexpected content + if (request.scheme === "https") { + if (query === "test1a") { + response.write(REDIRECT_META); + return; + } + + if (query === "test2a") { + response.write(REDIRECT_JS); + return; + } + + if (query === "test3a") { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", REDIRECT_302, false); + return; + } + + // Simulating a timeout by processing the https request + response.processAsync(); + return; + } + + // We should arrive here when the redirection was downraded successful + if (query == "testEnd") { + response.write(RESPONSE_SUCCESS); + return; + } + // We should never arrive here, just in case send 'error' + response.write(RESPONSE_UNEXPECTED); +} diff --git a/dom/security/test/https-first/file_referrer_policy.sjs b/dom/security/test/https-first/file_referrer_policy.sjs new file mode 100644 index 0000000000..282f7ba09f --- /dev/null +++ b/dom/security/test/https-first/file_referrer_policy.sjs @@ -0,0 +1,102 @@ +const RESPONSE_ERROR = ` + <html> + <body> + Error occurred... + <script type="application/javascript"> + window.opener.postMessage({result: 'ERROR'}, '*'); + </script> + </body> + </html>`; +const RESPONSE_POLICY = ` +<html> +<body> +Send policy onload... +<script type="application/javascript"> + const loc = document.location.href; + window.opener.postMessage({result: document.referrer, location: loc}, "*"); +</script> +</body> +</html>`; + +const expectedQueries = [ + "no-referrer", + "no-referrer-when-downgrade", + "origin", + "origin-when-cross-origin", + "same-origin", + "strict-origin", + "strict-origin-when-cross-origin", + "unsafe-url", +]; +function readQuery(testCase) { + let twoValues = testCase.split("-"); + let upgradeRequest = twoValues[0] === "https" ? 1 : 0; + let httpsResponse = twoValues[1] === "https" ? 1 : 0; + return [upgradeRequest, httpsResponse]; +} + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + Cu.importGlobalProperties(["URLSearchParams"]); + let query = new URLSearchParams(request.queryString); + // Downgrade to test http/https -> HTTP referrer policy + if (query.has("sendMe2") && request.scheme === "https") { + // Simulating a timeout by processing the https request + response.processAsync(); + return; + } + if (query.has("sendMe") || query.has("sendMe2")) { + response.write(RESPONSE_POLICY); + return; + } + // Get the referrer policy that we want to set + let referrerPolicy = query.get("rp"); + //If the query contained one of the expected referrer policies send a request with the given policy, + // else send error + if (expectedQueries.includes(referrerPolicy)) { + // Determine the test case, e.g. don't upgrade request but send response in https + let testCase = readQuery(query.get("upgrade")); + let httpsRequest = testCase[0]; + let httpsResponse = testCase[1]; + // Downgrade to http if upgrade equals 0 + if (httpsRequest === 0 && request.scheme === "https") { + // Simulating a timeout by processing the https request + response.processAsync(); + return; + } + // create js redirection that request with the given (related to the query) referrer policy + const SEND_REQUEST_HTTPS = ` + <html> + <head> + <meta name="referrer" content=${referrerPolicy}> + </head> + <body> + JS REDIRECT + <script> + let url = 'https://example.com/tests/dom/security/test/https-first/file_referrer_policy.sjs?sendMe'; + window.location = url; + </script> + </body> + </html>`; + const SEND_REQUEST_HTTP = ` + <html> + <head> + <meta name="referrer" content=${referrerPolicy}> + </head> + <body> + JS REDIRECT + <script> + let url = 'http://example.com/tests/dom/security/test/https-first/file_referrer_policy.sjs?sendMe2'; + window.location = url; + </script> + </body> + </html>`; + let respond = httpsResponse === 1 ? SEND_REQUEST_HTTPS : SEND_REQUEST_HTTP; + response.write(respond); + return; + } + + // We should never get here but in case we send an error + response.setStatusLine(request.httpVersion, 500, "OK"); + response.write(RESPONSE_ERROR); +} diff --git a/dom/security/test/https-first/file_slow_download.html b/dom/security/test/https-first/file_slow_download.html new file mode 100644 index 0000000000..084977607d --- /dev/null +++ b/dom/security/test/https-first/file_slow_download.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test slow download from an http site that gets upgraded to https</title> +</head> +<body> + <a href="http://example.com/browser/dom/security/test/https-first/file_slow_download.sjs" download="large-dummy-file.txt" id="testlink">download by attribute</a> + <script> + // click the link to start download + let testlink = document.getElementById("testlink"); + testlink.click(); + </script> + </body> +</html> diff --git a/dom/security/test/https-first/file_slow_download.sjs b/dom/security/test/https-first/file_slow_download.sjs new file mode 100644 index 0000000000..6e4f109068 --- /dev/null +++ b/dom/security/test/https-first/file_slow_download.sjs @@ -0,0 +1,26 @@ +"use strict"; +let timer; + +// Send a part of the file then wait for 3 second before sending the rest. +// If download isn't exempt from background timer of https-only/-first then the download +// gets cancelled before it completed. +const DELAY_MS = 3500; +function handleRequest(request, response) { + response.processAsync(); + timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader( + "Content-Disposition", + "attachment; filename=large-dummy-file.txt" + ); + response.setHeader("Content-Type", "text/plain"); + response.write("Begin the file"); + timer.init( + () => { + response.write("End of file"); + response.finish(); + }, + DELAY_MS, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/dom/security/test/https-first/file_toplevel_cookies.sjs b/dom/security/test/https-first/file_toplevel_cookies.sjs new file mode 100644 index 0000000000..dd9f7c0909 --- /dev/null +++ b/dom/security/test/https-first/file_toplevel_cookies.sjs @@ -0,0 +1,233 @@ +// Custom *.sjs file specifically for the needs of Bug 1711453 +"use strict"; + +// small red image +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +const IFRAME_INC = `<iframe id="testframeinc"></iframe>`; + +// Sets an image sends cookie and location after loading +const SET_COOKIE_IMG = ` +<html> +<body> +<img id="cookieImage"> +<script class="testbody" type="text/javascript"> + var cookieImage = document.getElementById("cookieImage"); + cookieImage.onload = function() { + let myLocation = window.location.href; + let myCookie = document.cookie; + window.opener.postMessage({result: 'upgraded', loc: myLocation, cookie: myCookie}, '*'); + } + cookieImage.onerror = function() { + window.opener.postMessage({result: 'error'}, '*'); + } + // Add the last number of the old query to the new query to set cookie properly + cookieImage.src = window.location.origin + "/tests/dom/security/test/https-first/file_toplevel_cookies.sjs?setSameSiteCookie" + + window.location.href.charAt(window.location.href.length -1); +</script> +</body> +</html> +`; + +// Load blank frame navigation sends cookie and location after loading +const LOAD_BLANK_FRAME_NAV = ` +<html> +<body> +<iframe id="testframe"></iframe> +<script> + let testframe = document.getElementById("testframe"); + testframe.onload = function() { + let myLocation = window.location.href; + let myCookie = document.cookie; + window.opener.postMessage({result: 'upgraded', loc: myLocation, cookie: myCookie}, '*'); + } + testframe.onerror = function() { + window.opener.postMessage({result: 'error', loc: 'error', cookie: ''}, '*'); + } + testframe.src = window.location.origin + "/tests/dom/security/test/https-first/file_toplevel_cookies.sjs?loadblankframeNav"; +</script> +</body> +</html> +`; + +// Load frame navigation sends cookie and location after loading +const LOAD_FRAME_NAV = ` +<html> +<body> +<iframe id="testframe"></iframe> +<script> + let testframe = document.getElementById("testframe"); + testframe.onload = function() { + let myLocation = window.location.href; + let myCookie = document.cookie; + window.opener.postMessage({result: 'upgraded', loc: myLocation, cookie: myCookie}, '*'); + } + testframe.onerror = function() { + window.opener.postMessage({result: 'error', loc: 'error', cookie: ''}, '*'); + } + testframe.src = window.location.origin + "/tests/dom/security/test/https-first/file_toplevel_cookies.sjs?loadsrcdocframeNav"; +</script> +</body> +</html> + +`; +// blank frame sends cookie and location after loading +const LOAD_BLANK_FRAME = ` +<html> +<body> +<iframe id="testframe"></iframe> +<script> + let testframe = document.getElementById("testframe"); + testframe.onload = function() { + let myLocation = window.location.href; + let myCookie = document.cookie; + window.opener.postMessage({result: 'upgraded', loc: myLocation, cookie: myCookie}, '*'); + } + testframe.onerror = function() { + window.opener.postMessage({result: 'error', loc: 'error', cookie: ''}, '*'); + } + testframe.src = window.location.origin + "/tests/dom/security/test/https-first/file_toplevel_cookies.sjs?loadblankframeInc"; +</script> +</body> +</html> +`; +// frame sends cookie and location after loading +const LOAD_FRAME = ` +<html> +<body> +<iframe id="testframe"></iframe> +<script> + let testframe = document.getElementById("testframe"); + testframe.onload = function() { + let myLocation = window.location.href; + let myCookie = document.cookie; + window.opener.postMessage({result: 'upgraded', loc: myLocation, cookie: myCookie}, '*'); + } + testframe.onerror = function() { + window.opener.postMessage({result: 'error', loc: 'error', cookie: ''}, '*'); + } + testframe.src = window.location.origin + "/tests/dom/security/test/https-first/file_toplevel_cookies.sjs?loadsrcdocframeInc"; +</script> +</body> +</html> +`; + +const RESPONSE_UNEXPECTED = ` + <html> + <body> + send message, error + <script type="application/javascript"> + let myLocation = document.location.href; + window.opener.postMessage({result: 'error', loc: myLocation}, '*'); + </script> + </body> + </html>`; + +function setCookie(name, query) { + let cookie = name + "="; + if (query.includes("0")) { + cookie += "0;Domain=.example.com;sameSite=none"; + return cookie; + } + if (query.includes("1")) { + cookie += "1;Domain=.example.com;sameSite=strict"; + return cookie; + } + if (query.includes("2")) { + cookie += "2;Domain=.example.com;sameSite=none;secure"; + return cookie; + } + if (query.includes("3")) { + cookie += "3;Domain=.example.com;sameSite=strict;secure"; + return cookie; + } + return cookie + "error"; +} + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + let query = request.queryString; + if (query.includes("setImage")) { + response.write(SET_COOKIE_IMG); + return; + } + // using startsWith and discard the math random + if (query.includes("setSameSiteCookie")) { + response.setHeader("Set-Cookie", setCookie("setImage", query), true); + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + return; + } + + // navigation tests + if (query.includes("loadNavBlank")) { + response.setHeader("Set-Cookie", setCookie("loadNavBlank", query), true); + response.write(LOAD_BLANK_FRAME_NAV); + return; + } + + if (request.queryString === "loadblankframeNav") { + let FRAME = ` + <iframe src="about:blank" + // nothing happens here + </iframe>`; + response.write(FRAME); + return; + } + + if (query.includes("loadNav")) { + response.setHeader("Set-Cookie", setCookie("loadNav", query), true); + response.write(LOAD_FRAME_NAV); + return; + } + + if (query === "loadsrcdocframeNav") { + let FRAME = ` + <iframe srcdoc="foo" + // nothing happens here + </iframe>`; + response.write(FRAME); + return; + } + + // inclusion tests + if (query.includes("loadframeIncBlank")) { + response.setHeader( + "Set-Cookie", + setCookie("loadframeIncBlank", query), + true + ); + response.write(LOAD_BLANK_FRAME); + return; + } + + if (request.queryString === "loadblankframeInc") { + let FRAME = + ` <iframe id="blankframe" src="about:blank"></iframe> + <script> + document.getElementById("blankframe").contentDocument.write("` + + IFRAME_INC + + `"); + <\script>`; + response.write(FRAME); + return; + } + + if (query.includes("loadframeInc")) { + response.setHeader("Set-Cookie", setCookie("loadframeInc", query), true); + response.write(LOAD_FRAME); + return; + } + + if (request.queryString === "loadsrcdocframeInc") { + response.write('<iframe srcdoc="' + IFRAME_INC + '"></iframe>'); + return; + } + + // We should never arrive here, just in case send 'error' + response.write(RESPONSE_UNEXPECTED); +} diff --git a/dom/security/test/https-first/file_upgrade_insecure.html b/dom/security/test/https-first/file_upgrade_insecure.html new file mode 100644 index 0000000000..af306d2a16 --- /dev/null +++ b/dom/security/test/https-first/file_upgrade_insecure.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1704454 - HTTPS FIRST Mode</title> + <!-- style --> + <link rel='stylesheet' type='text/css' href='http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?style' media='screen' /> + + <!-- font --> + <style> + @font-face { + font-family: "foofont"; + src: url('http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?font'); + } + .div_foo { font-family: "foofont"; } + </style> +</head> +<body> + + <!-- images: --> + <img src="http://example.com/tests/dom/security/test/https-first/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/https-first/file_upgrade_insecure_server.sjs?redirect-image"></img> + + <!-- script: --> + <script src="http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?script"></script> + + <!-- media: --> + <audio src="http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?media"></audio> + + <!-- objects: --> + <object width="10" height="10" data="http://example.com/tests/dom/security/test/https-first/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/https-first/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> + + <!-- toplevel: --> + <script type="application/javascript"> + let myWin = window.open("http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?top-level"); + //close right after opening + myWin.onunload = function(){ + myWin.close(); + } + </script> + + <!-- xhr: --> + <script type="application/javascript"> + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "http://example.com/tests/dom/security/test/https-first/file_upgrade_insecure_server.sjs?xhr"); + myXHR.send(null); + </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/https-first/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/https-first/file_upgrade_insecure_server.sjs b/dom/security/test/https-first/file_upgrade_insecure_server.sjs new file mode 100644 index 0000000000..1f14dc635d --- /dev/null +++ b/dom/security/test/https-first/file_upgrade_insecure_server.sjs @@ -0,0 +1,114 @@ +// SJS file for https-first Mode mochitests +// Bug 1704454 - HTTPS First Mode + +const TOTAL_EXPECTED_REQUESTS = 12; + +const IFRAME_CONTENT = + "<!DOCTYPE HTML>" + + "<html>" + + "<head><meta charset='utf-8'>" + + "<title>Bug 1704454 - Test HTTPS First Mode</title>" + + "</head>" + + "<body>" + + "<img src='http://example.com/tests/dom/security/test/https-first/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", + "top-level", +]; + +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/https-first/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.includes(queryString)) { + response.write("unexpected-response"); + return; + } + + // make sure all the requested queries aren't upgraded to https + // except of toplevel requests + if (queryString === "top-level") { + queryString += request.scheme === "https" ? "-ok" : "-error"; + } else { + queryString += request.scheme === "http" ? "-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. + + // 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/https-first/mochitest.ini b/dom/security/test/https-first/mochitest.ini new file mode 100644 index 0000000000..504f8856b9 --- /dev/null +++ b/dom/security/test/https-first/mochitest.ini @@ -0,0 +1,43 @@ +[DEFAULT] + +[test_fragment.html] +support-files = file_fragment.html +[test_resource_upgrade.html] +scheme=https +support-files = + file_upgrade_insecure.html + file_upgrade_insecure_server.sjs +skip-if = true # Bug 1727101, Bug 1727925 +[test_redirect_upgrade.html] +scheme=https +support-files = + file_redirect.sjs +[test_redirect_downgrade.html] +support-files = file_redirect_downgrade.sjs +[test_data_uri.html] +support-files = + file_data_uri.html + [test_toplevel_cookies.html] +support-files = + file_toplevel_cookies.sjs +[test_downgrade_bad_responses.html] +support-files= file_downgrade_bad_responses.sjs +[test_referrer_policy.html] +support-files= file_referrer_policy.sjs +[test_break_endless_upgrade_downgrade_loop.html] +support-files = + file_break_endless_upgrade_downgrade_loop.sjs + file_downgrade_with_different_path.sjs +[test_multiple_redirection.html] +support-files = + file_multiple_redirection.sjs +[test_form_submission.html] +support-files = + file_form_submission.sjs +[test_bad_cert.html] +support-files = + file_bad_cert.sjs +[test_downgrade_request_upgrade_request.html] +support-files = file_downgrade_request_upgrade_request.sjs +[test_downgrade_500_responses.html] +support-files = file_downgrade_500_responses.sjs diff --git a/dom/security/test/https-first/pass.png b/dom/security/test/https-first/pass.png Binary files differnew file mode 100644 index 0000000000..2fa1e0ac06 --- /dev/null +++ b/dom/security/test/https-first/pass.png diff --git a/dom/security/test/https-first/test.ogv b/dom/security/test/https-first/test.ogv Binary files differnew file mode 100644 index 0000000000..0f83996e5d --- /dev/null +++ b/dom/security/test/https-first/test.ogv diff --git a/dom/security/test/https-first/test.wav b/dom/security/test/https-first/test.wav Binary files differnew file mode 100644 index 0000000000..85dc1ea904 --- /dev/null +++ b/dom/security/test/https-first/test.wav diff --git a/dom/security/test/https-first/test_bad_cert.html b/dom/security/test/https-first/test_bad_cert.html new file mode 100644 index 0000000000..d7e9296d97 --- /dev/null +++ b/dom/security/test/https-first/test_bad_cert.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1719309 +Test that bad cert sites won't get upgraded by https-first +--> + +<head> + <title>HTTPS-FirstMode - Bad Certificates</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + <h1>HTTPS-First Mode</h1> + <p>Test: Downgrade bad certificates without warning page </p> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1706351">Bug 1719309</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + /* + * We perform the following tests: + * 1. Request nocert.example.com which is a site without a certificate + * 2. Request a site with self-signed cert (self-signed.example.com) + * 3. Request a site with an untrusted cert (untrusted.example.com) + * 4. Request a site with an expired cert + * 5. Request a site with an untrusted and expired cert + * 6. Request a site with no subject alternative dns name matching + * + * Expected result: Https-first tries to upgrade each request. Receives for each one an SSL_ERROR_* + * and downgrades back to http. + */ + const badCertificates = ["nocert","self-signed", "untrusted","expired","untrusted-expired", "no-subject-alt-name"]; + let currentTest = 0; + let testWin; + window.addEventListener("message", receiveMessage); + + // Receive message and verify that it is from an http site. + // Verify that we got the correct message and an http scheme + async function receiveMessage(event) { + let data = event.data; + let currentBadCert = badCertificates[currentTest]; + ok(data.result === "downgraded", "Downgraded request " + currentBadCert); + ok(data.scheme === "http:", "Received 'http' for " + currentBadCert); + testWin.close(); + if (++currentTest < badCertificates.length) { + startTest(); + return; + } + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); + } + + async function startTest() { + const currentCode = badCertificates[currentTest]; + // make a request to a subdomain of example.com with a bad certificate + testWin = window.open(`http://${currentCode}.example.com/tests/dom/security/test/https-first/file_bad_cert.sjs`); + } + + // Set preference and start test + SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_first", true], + ]}, startTest); + SimpleTest.waitForExplicitFinish(); + </script> +</body> +</html> diff --git a/dom/security/test/https-first/test_break_endless_upgrade_downgrade_loop.html b/dom/security/test/https-first/test_break_endless_upgrade_downgrade_loop.html new file mode 100644 index 0000000000..7d239350a1 --- /dev/null +++ b/dom/security/test/https-first/test_break_endless_upgrade_downgrade_loop.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1715253 +Test that same origin redirect does not cause endless loop with https-first enabled +--> + +<head> + <title>HTTPS-First-Mode - Break endless upgrade downgrade redirect loop</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + <h1>HTTPS-First Mode</h1> + <p>Upgrade Test for insecure redirects.</p> + + <script class="testbody" type="text/javascript"> + "use strict"; + + SimpleTest.waitForExplicitFinish(); + + const redirectCodes = ["301", "302","303","307"]; + let currentTest = 0; + let testWin; + window.addEventListener("message", receiveMessage); + + // receive message from loaded site verifying the scheme of + // the loaded document. + async function receiveMessage(event) { + let currentRedirectCode = redirectCodes[currentTest]; + is(event.data.result, + "scheme-http", + "same-origin redirect results in 'http' for " + currentRedirectCode + ); + testWin.close(); + if (++currentTest < redirectCodes.length) { + startTest(); + return; + } + window.removeEventListener("message", receiveMessage); + window.addEventListener("message", receiveMessageForDifferentPathTest); + testDifferentPath(); + } + + async function receiveMessageForDifferentPathTest(event) { + is(event.data.result, + "scheme-https", + "scheme should be https when the path is different" + ); + testWin.close(); + window.removeEventListener("message", receiveMessageForDifferentPathTest); + SimpleTest.finish(); + } + + async function startTest() { + const currentCode = redirectCodes[currentTest]; + // Load an http:// window which gets upgraded to https:// + let uri = + `http://example.com/tests/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs?${currentCode}`; + testWin = window.open(uri); + } + + async function testDifferentPath() { + // Load an https:// window which gets downgraded to http:// + let uri = + `https://example.com/tests/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs?downgrade`; + testWin = window.open(uri); + } + + // Set preference and start test + SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_first", true], + ["security.mixed_content.block_active_content", false], + ["security.mixed_content.block_display_content", false], + ["dom.security.https_only_check_path_upgrade_downgrade_endless_loop", true], + ]}, startTest); + </script> +</body> +</html> diff --git a/dom/security/test/https-first/test_data_uri.html b/dom/security/test/https-first/test_data_uri.html new file mode 100644 index 0000000000..b9891260db --- /dev/null +++ b/dom/security/test/https-first/test_data_uri.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1709069: Test that Data URI which makes a top-level request gets updated in https-first</title> + <script 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(); +window.addEventListener("message", receiveMessage); + +// HTML site which makes a top-level http request +const HTML = ` +<html> +<body> + DATA HTML +<script> + window.open("http://example.com/tests/dom/security/test/https-first/file_data_uri.html"); +<\/script> +<\/body> +<\/html> +`; + +const DATA_HTML = "data:text/html, " + HTML; + +// Verify that data uri top-level request got upgraded to https and +// the reached location is correct +async function receiveMessage(event){ + let data = event.data; + is(data.location, "https://example.com/tests/dom/security/test/https-first/file_data_uri.html", + "Reached the correct location"); + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +function test_toplevel_https() { + document.getElementById("testframe").src = DATA_HTML; +} + +SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_first", true], + ]}, test_toplevel_https); + + +</script> +</body> +</html> diff --git a/dom/security/test/https-first/test_downgrade_500_responses.html b/dom/security/test/https-first/test_downgrade_500_responses.html new file mode 100644 index 0000000000..3943c9095c --- /dev/null +++ b/dom/security/test/https-first/test_downgrade_500_responses.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1747673 : HTTPS First fallback to http for non-standard 5xx status code responses</title> +<script 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"> +"use strict"; +/* + * Description of the test: + * Perform five tests where https-first receives an + * 5xx status code (standard and non-standard 5xx status) if request is send to site by https. + * Expected behaviour: https-first fallbacks to http after receiving 5xx error. + * Test 1: 501 Response + * Test 2: 504 Response + * Test 3: 521 Response + * Test 4: 530 Response + * Test 5: 560 Response + */ + +SimpleTest.waitForExplicitFinish(); + +const REQUEST_URL = + "http://example.com/tests/dom/security/test/https-first/file_downgrade_500_responses.sjs"; + +const redirectQueries = ["?test1a", "?test2a","?test3a", "?test4a", "?test5a"]; +let currentTest = 0; +let testWin; +let currentQuery; +window.addEventListener("message", receiveMessage); + +// Receive message and verify that it is from an http site. +// When the message is 'downgraded' then it was send by an http site +// and the redirection worked. +async function receiveMessage(event) { + let data = event.data; + currentQuery = redirectQueries[currentTest]; + ok(data.result === "downgraded", "Redirected successful to 'http' for " + currentQuery); + is(data.scheme, "http:", "scheme is 'http' for " + currentQuery ); + testWin.close(); + if (++currentTest < redirectQueries.length) { + runTest(); + return; + } + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +async function runTest() { + currentQuery = redirectQueries[currentTest]; + testWin = window.open(REQUEST_URL + currentQuery, "_blank"); +} + +SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_first", true] + ]}, runTest); + +</script> +</body> +</html> diff --git a/dom/security/test/https-first/test_downgrade_bad_responses.html b/dom/security/test/https-first/test_downgrade_bad_responses.html new file mode 100644 index 0000000000..39cef7f26a --- /dev/null +++ b/dom/security/test/https-first/test_downgrade_bad_responses.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1709552 : HTTPS-First: Add downgrade tests for bad responses to https request </title> +<script 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"> +"use strict"; +/* + * Description of the test: + * We perform five tests where we expect https-first to detect + * that the target site only supports http + * Test 1: 400 Response + * Test 2: 401 Response + * Test 3: 403 Response + * Test 4: 416 Response + * Test 5: 418 Response + * Test 6: Timeout + */ + +SimpleTest.waitForExplicitFinish(); + +const REQUEST_URL = + "http://example.com/tests/dom/security/test/https-first/file_downgrade_bad_responses.sjs"; + +const redirectQueries = ["?test1a", "?test2a","?test3a", "?test4a", "?test5a", "?test6a"]; +let currentTest = 0; +let testWin; +let currentQuery; +window.addEventListener("message", receiveMessage); + +// Receive message and verify that it is from an http site. +// When the message is 'downgraded' then it was send by an http site +// and the redirection worked. +async function receiveMessage(event) { + let data = event.data; + currentQuery = redirectQueries[currentTest]; + ok(data.result === "downgraded", "Redirected successful to 'http' for " + currentQuery); + ok(data.scheme === "http", "scheme is 'http' for " + currentQuery ); + testWin.close(); + if (++currentTest < redirectQueries.length) { + runTest(); + return; + } + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +async function runTest() { + currentQuery = redirectQueries[currentTest]; + testWin = window.open(REQUEST_URL + currentQuery, "_blank"); +} + +SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_first", true] + ]}, runTest); + +</script> +</body> +</html> diff --git a/dom/security/test/https-first/test_downgrade_request_upgrade_request.html b/dom/security/test/https-first/test_downgrade_request_upgrade_request.html new file mode 100644 index 0000000000..b659636ace --- /dev/null +++ b/dom/security/test/https-first/test_downgrade_request_upgrade_request.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<head> +<title> Bug 1706126: Test https-first, downgrade first request and then upgrade redirection to subdomain</title> +<script 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"> +"use strict"; +/* + * Description of the test: + * First we request http://redirect-example.com which HTTPS-First upgrades to https://redirect-example.com. + * The request https://redirect-example.com doesn't receive an answer (timeout), so we send a background + * request. + * The background request receives an answer. So the request https://redirect-example.com gets downgraded + * to http://redirect-example.com by the exempt flag. + * The request http://redirect-example.com gets redirected to http://wwww.redirect-example.com. At that stage + * HTTPS-First should clear the exempt flag and upgrade the redirection to https://wwww.redirect-example.com. + * + */ + +SimpleTest.waitForExplicitFinish(); + +const REQUEST_URL = + "http://redirect-example.com/tests/dom/security/test/https-first/file_downgrade_request_upgrade_request.sjs"; + +let testWin; +window.addEventListener("message", receiveMessage); + +// Receive message and verify that it is from an https site. +async function receiveMessage(event) { + let data = event.data; + ok(data.result === "upgraded", "Redirected successful to 'https' for subdomain "); + is(data.scheme,"https:", "scheme is 'https' for subdomain"); + testWin.close(); + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +async function runTest() { + testWin = window.open(REQUEST_URL, "_blank"); +} + +SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_first", true] + ]}, runTest); + +</script> +</body> +</html> diff --git a/dom/security/test/https-first/test_form_submission.html b/dom/security/test/https-first/test_form_submission.html new file mode 100644 index 0000000000..a68c3501c6 --- /dev/null +++ b/dom/security/test/https-first/test_form_submission.html @@ -0,0 +1,122 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1720103 - Https-first: Do not upgrade form submissions (for now)</title> + <script 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 test https-first behaviour with forms. + * We perform each test with once with same origin and the second time + * with a cross origin. We perform two GET form requests and two POST + * form requests. + * In more detail: + * + * 1. Test: Request that gets upgraded to https, GET form submission. + * + * 2. Test: Request that gets upgraded to https, that upgraded request + * gets timed out, so https-first send an http request, GET form submission. + * + * 3. Test: request that gets upgraded to https, and sends a POST form + * to http://example.com. + * + * 4. Test: Request where the https upgrade get timed out -> http, and sends a POST form + * to http://example.com, + * + */ +SimpleTest.waitForExplicitFinish(); +window.addEventListener("message", receiveMessage); + +const SAME_ORIGIN = "http://example.com/tests/dom/security/test/https-first/file_form_submission.sjs"; +const CROSS_ORIGIN = SAME_ORIGIN.replace(".com", ".org"); +const Tests = [{ + // 1. Test GET, gets upgraded + query: "?test=1", + scheme: "https:", + method: "GET", + value: "test=success", +}, +{ + // 2. Test GET, initial request will be downgraded + query:"?test=2", + scheme: "http:", + method: "GET", + value: "test=success" +}, +{ // 3. Test POST formular, gets upgraded + query: "?test=3", + scheme: "http:", + method: "POST", + value: "test=success" +}, +{ // 4. Test POST formular, request will be downgraded + query: "?test=4", + scheme: "http:", + method: "POST", + value: "test=success" +}, +]; +let currentTest; +let counter = 0; +let testWin; +let sameOrigin = true; + +// Verify that top-level request got the expected scheme and reached the correct location. +async function receiveMessage(event){ + let data = event.data; + let origin = sameOrigin? SAME_ORIGIN : CROSS_ORIGIN + const expectedLocation = origin.replace("http:", currentTest.scheme); + // If GET request check that form was transfered by url + if (currentTest.method === "GET") { + is(data.location, expectedLocation + currentTest.query, + "Reached the correct location for " + currentTest.query ); + } else { + // Since the form is always send to example.com we expect it here as location + is(data.location.includes(SAME_ORIGIN.replace("http:", currentTest.scheme)), true, + "Reached the correct location for " + currentTest.query ); + } + is(data.scheme, currentTest.scheme,`${currentTest.query} upgraded or downgraded to ` + currentTest.scheme); + // Check that the form value is correct + is(data.form, currentTest.value, "Form was transfered"); + testWin.close(); + // Flip origin flag + sameOrigin ^= true; + // Only go to next test if already sent same and cross origin request for current test + if (sameOrigin) { + counter++; + } + // Check if we have test left, if not finish the testing + if (counter >= Tests.length) { + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); + return; + } + // If we didn't reached the end yet, run next test + runTest(); +} + +function runTest() { + currentTest = Tests[counter]; + // If sameOrigin flag is set make a origin request, else a cross origin request + if (sameOrigin) { + testWin= window.open(SAME_ORIGIN + currentTest.query, "_blank"); + } else { + testWin= window.open(CROSS_ORIGIN + currentTest.query, "_blank"); + } +} + +// Set prefs and start test +SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_first", true], + ["security.warn_submit_secure_to_insecure", false] + ]}, runTest); + + +</script> +</body> +</html> diff --git a/dom/security/test/https-first/test_fragment.html b/dom/security/test/https-first/test_fragment.html new file mode 100644 index 0000000000..63a7affee2 --- /dev/null +++ b/dom/security/test/https-first/test_fragment.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1706577: Have https-first mode account for fragment navigations</title> +<script 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"> +"use strict"; +/* + * Description of the test: + * Have https-first detect a fragment navigation rather than navigating away + * from the page. + */ + +SimpleTest.waitForExplicitFinish(); + +const REQUEST_URL = "http://example.com/tests/dom/security/test/https-first/file_fragment.html"; +const EXPECT_URL = REQUEST_URL.replace("http://", "https://"); + +let winTest = null; +let checkButtonClicked = false; + +async function receiveMessage(event) { + let data = event.data; + if (!checkButtonClicked) { + ok(data.result == EXPECT_URL, "location is correct"); + ok(data.button, "button is clicked"); + ok(data.info == "onload", "Onloading worked"); + checkButtonClicked = true; + return; + } + + // Once the button was clicked we know the tast has finished + ok(data.button, "button is clicked"); + ok(data.result == "#foo", "location (hash) is correct"); + ok(data.info == "scrolled-to-foo","Scrolled successfully without reloading!"); + window.removeEventListener("message",receiveMessage); + winTest.close(); + SimpleTest.finish(); +} + +async function runTest() { + await SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_first", true], + ]}); + winTest = window.open(REQUEST_URL); +} + +window.addEventListener("message", receiveMessage); + +runTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/https-first/test_multiple_redirection.html b/dom/security/test/https-first/test_multiple_redirection.html new file mode 100644 index 0000000000..d631f140e6 --- /dev/null +++ b/dom/security/test/https-first/test_multiple_redirection.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1721410 +Test multiple redirects using https-first and ensure the entire redirect chain is using https +--> + +<head> + <title>HTTPS-First-Mode - Test for multiple redirections</title> + <script 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"> + "use strict"; + + SimpleTest.waitForExplicitFinish(); + + const testCase = [ + // test 1: https-first upgrades http://example.com/test1 -> https://example.com/test1 + // that's redirect to https://example.com/.../redirect which then redirects + // to http://example.com/../verify. Since the last redirect is http, and the + // the redirection chain contains already example.com we expect https-first + // to downgrade the request. + {name: "test last redirect HTTP", result: "scheme-http", query: "test1" }, + // test 2: https-first upgrades http://example.com/test2 -> https://example.com/test2 + // that's redirect to https://example.com/.../redirect which then redirects + // to https://example.com/../verify. Since the last redirect is https, we + // expect to reach an https website. + {name: "test last redirect HTTPS", result: "scheme-https", query: "test2"}, + // test 3: https-first upgrades http://example.com/test3 -> https://example.com/test3 + // that's redirect to https://example.com/.../hsts which then sets an hsts header + // and redirects to http://example.com/../verify. Since an hsts header was set + // we expect that to reach an https site + {name: "test last redirect HSTS", result: "scheme-https", query: "test3"}, + // reset: reset hsts header for example.com + {name: "reset HSTS header", result: "scheme-https", query: "reset"}, + ] + let currentTest = 0; + let testWin; + window.addEventListener("message", receiveMessage); + + // receive message from loaded site verifying the scheme of + // the loaded document. + async function receiveMessage(event) { + let test = testCase[currentTest]; + is(event.data.result, + test.result, + "same-origin redirect results in " + test.name + ); + testWin.close(); + if (++currentTest < testCase.length) { + startTest(); + return; + } + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); + } + + async function startTest() { + const test = testCase[currentTest]; + // Load an http:// window which gets upgraded to https:// + let uri = + `http://example.com/tests/dom/security/test/https-first/file_multiple_redirection.sjs?${test.query}`; + testWin = window.open(uri); + } + + // Set preference and start test + SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_first", true], + ]}, startTest); + </script> +</body> +</html> diff --git a/dom/security/test/https-first/test_redirect_downgrade.html b/dom/security/test/https-first/test_redirect_downgrade.html new file mode 100644 index 0000000000..07f998c085 --- /dev/null +++ b/dom/security/test/https-first/test_redirect_downgrade.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1707856: Test redirect downgrades with https-first</title> +<script 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"> +"use strict"; +/* + * Description of the test: + * We perform three tests where we expect https-first to detect + * that the target site only supports http + * Test 1: Meta Refresh + * Test 2: JS Redirect + * Test 3: 302 redirect + */ + +SimpleTest.waitForExplicitFinish(); + +const REQUEST_URL = + "http://example.com/tests/dom/security/test/https-first/file_redirect_downgrade.sjs"; + +const redirectQueries = ["?test1a", "?test2a","?test3a"]; +let currentTest = 0; +let testWin; +let currentQuery; +window.addEventListener("message", receiveMessage); + +// Receive message and verify that it is from an https site. +// When the message is 'downgraded' then it was send by an http site +// and the redirection worked. +async function receiveMessage(event) { + let data = event.data; + ok(data.result === "downgraded", "Redirected successful to 'http' for " + currentQuery); + ok(data.scheme === "http:", "scheme is 'http' for " + currentQuery ); + testWin.close(); + if (++currentTest < redirectQueries.length) { + runTest(); + return; + } + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +async function runTest() { + currentQuery = redirectQueries[currentTest]; + testWin = window.open(REQUEST_URL + currentQuery, "_blank"); +} + +SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_first", true] + ]}, runTest); + +</script> +</body> +</html> diff --git a/dom/security/test/https-first/test_redirect_upgrade.html b/dom/security/test/https-first/test_redirect_upgrade.html new file mode 100644 index 0000000000..6cccf6af67 --- /dev/null +++ b/dom/security/test/https-first/test_redirect_upgrade.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1706351 +Test that 302 redirect requests get upgraded to https:// with HTTPS-First Mode enabled +--> + +<head> + <title>HTTPS-FirstMode - Redirect Upgrade</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + <h1>HTTPS-First Mode</h1> + <p>Upgrade Test for insecure redirects.</p> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1706351">Bug 1706351</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + const redirectCodes = ["301", "302","303","307"]; + let currentTest = 0; + let testWin; + window.addEventListener("message", receiveMessage); + + // Receive message and verify that it is from an https site. + // When the message is 'secure' then it was send by an https site. + async function receiveMessage(event) { + let data = event.data; + let currentRedirectCode = redirectCodes[currentTest]; + ok(data.result === "secure", "Received 'https' for " + currentRedirectCode); + testWin.close(); + if (++currentTest < redirectCodes.length) { + startTest(); + return; + } + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); + } + + async function startTest() { + const currentCode = redirectCodes[currentTest]; + // Make a request to a site (eg. https://file_redirect.sjs?301), which will redirect to http://file_redirect.sjs?check. + // The response will either be secure-ok, if the request has been upgraded to https:// or secure-error if it didn't. + testWin = window.open(`https://example.com/tests/dom/security/test/https-first/file_redirect.sjs?${currentCode}`); + } + + // Set preference and start test + SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_first", true], + ["security.mixed_content.block_active_content", false], + ["security.mixed_content.block_display_content", false], + ]}, startTest); + SimpleTest.waitForExplicitFinish(); + </script> +</body> +</html> diff --git a/dom/security/test/https-first/test_referrer_policy.html b/dom/security/test/https-first/test_referrer_policy.html new file mode 100644 index 0000000000..61521e2351 --- /dev/null +++ b/dom/security/test/https-first/test_referrer_policy.html @@ -0,0 +1,237 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1716706 : Write referrer-policy tests for https-first </title> +<script 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"> +"use strict"; +/* + * Description of the test: + * We perform each test with 8 different settings. + * The first is a same origin request from an http site to an https site. + * The second is a same origin request from an https -> https. + * The third is a cross-origin request from an http -> https. + * The fourth is a cross-origin request from an https -> https. + * The fifth is a same origin request from an http -> http site. + * The sixth is a same origin request from an https -> http. + * The seventh is a cross-origin request from an http -> http. + * The last is a cross-origin request from an https -> http. + */ + +SimpleTest.waitForExplicitFinish(); +// This test performs a lot of requests and checks (64 requests). +// So to prevent to get a timeout before executing all test request longer timeout. +SimpleTest.requestLongerTimeout(2); +const SAME_ORIGIN = + "http://example.com/tests/dom/security/test/https-first/file_referrer_policy.sjs?"; +// SAME ORIGIN with "https" instead of "http" +const SAME_ORIGIN_HTTPS = SAME_ORIGIN.replace("http", "https"); + +const CROSS_ORIGIN = + "http://example.org/tests/dom/security/test/https-first/file_referrer_policy.sjs?"; +// CROSS ORIGIN with "https" instead of "http" +const CROSS_ORIGIN_HTTPS = CROSS_ORIGIN.replace("http", "https"); + +// Define test cases. Query equals the test case referrer policy. +// We will set in the final request the url parameters such that 'rp=' equals the referrer policy +//and 'upgrade=' equals '1' if the request should be https. +// For a 'upgrade=0' url parameter the server lead to a timeout such that https-first downgrades +// the request to http. +const testCases = [ + { + query: "no-referrer", + expectedResultSameOriginDownUp: "", + expectedResultSameOriginUpUp: "", + expectedResultCrossOriginDownUp:"", + expectedResultCrossOriginUpUp:"", + expectedResultSameOriginDownDown: "", + expectedResultSameOriginUpDown: "", + expectedResultCrossOriginDownDown:"", + expectedResultCrossOriginUpDown: "", + }, + { + query: "no-referrer-when-downgrade", + expectedResultSameOriginDownUp: SAME_ORIGIN + "rp=no-referrer-when-downgrade&upgrade=http-https", + expectedResultSameOriginUpUp: SAME_ORIGIN_HTTPS + "rp=no-referrer-when-downgrade&upgrade=https-https", + expectedResultCrossOriginDownUp: CROSS_ORIGIN + "rp=no-referrer-when-downgrade&upgrade=http-https", + expectedResultCrossOriginUpUp: CROSS_ORIGIN_HTTPS + "rp=no-referrer-when-downgrade&upgrade=https-https", + expectedResultSameOriginDownDown: SAME_ORIGIN + "rp=no-referrer-when-downgrade&upgrade=http-http", + expectedResultSameOriginUpDown: "", + expectedResultCrossOriginDownDown: CROSS_ORIGIN + "rp=no-referrer-when-downgrade&upgrade=http-http", + expectedResultCrossOriginUpDown:"", + }, + { + query: "origin", + expectedResultSameOriginDownUp: "http://example.com/", + expectedResultSameOriginUpUp: "https://example.com/", + expectedResultCrossOriginDownUp:"http://example.org/", + expectedResultCrossOriginUpUp:"https://example.org/", + expectedResultSameOriginDownDown: "http://example.com/", + expectedResultSameOriginUpDown: "https://example.com/", + expectedResultCrossOriginDownDown:"http://example.org/", + expectedResultCrossOriginUpDown:"https://example.org/", + }, + { + query: "origin-when-cross-origin", + expectedResultSameOriginDownUp: "http://example.com/", + expectedResultSameOriginUpUp: SAME_ORIGIN_HTTPS + "rp=origin-when-cross-origin&upgrade=https-https", + expectedResultCrossOriginDownUp:"http://example.org/", + expectedResultCrossOriginUpUp:"https://example.org/", + expectedResultSameOriginDownDown: SAME_ORIGIN + "rp=origin-when-cross-origin&upgrade=http-http", + expectedResultSameOriginUpDown: "https://example.com/", + expectedResultCrossOriginDownDown:"http://example.org/", + expectedResultCrossOriginUpDown:"https://example.org/", + }, + { + query: "same-origin", + expectedResultSameOriginDownUp: "", + expectedResultSameOriginUpUp: SAME_ORIGIN_HTTPS + "rp=same-origin&upgrade=https-https", + expectedResultCrossOriginDownUp:"", + expectedResultCrossOriginUpUp:"", + expectedResultSameOriginDownDown: SAME_ORIGIN + "rp=same-origin&upgrade=http-http", + expectedResultSameOriginUpDown: "", + expectedResultCrossOriginDownDown: "", + expectedResultCrossOriginUpDown:"", + }, + { + query: "strict-origin", + expectedResultSameOriginDownUp: "http://example.com/", + expectedResultSameOriginUpUp: "https://example.com/", + expectedResultCrossOriginDownUp:"http://example.org/", + expectedResultCrossOriginUpUp:"https://example.org/", + expectedResultSameOriginDownDown: "http://example.com/", + expectedResultSameOriginUpDown: "", + expectedResultCrossOriginDownDown:"http://example.org/", + expectedResultCrossOriginUpDown:"", + }, + { + query: "strict-origin-when-cross-origin", + expectedResultSameOriginDownUp: "http://example.com/", + expectedResultSameOriginUpUp: SAME_ORIGIN_HTTPS + "rp=strict-origin-when-cross-origin&upgrade=https-https", + expectedResultCrossOriginDownUp:"http://example.org/", + expectedResultCrossOriginUpUp:"https://example.org/", + expectedResultSameOriginDownDown: SAME_ORIGIN + "rp=strict-origin-when-cross-origin&upgrade=http-http", + expectedResultSameOriginUpDown: "", + expectedResultCrossOriginDownDown:"http://example.org/", + expectedResultCrossOriginUpDown:"", + }, + { + query: "unsafe-url", + expectedResultSameOriginDownUp: SAME_ORIGIN + "rp=unsafe-url&upgrade=http-https", + expectedResultSameOriginUpUp: SAME_ORIGIN_HTTPS + "rp=unsafe-url&upgrade=https-https", + expectedResultCrossOriginDownUp: CROSS_ORIGIN + "rp=unsafe-url&upgrade=http-https", + expectedResultCrossOriginUpUp: CROSS_ORIGIN_HTTPS + "rp=unsafe-url&upgrade=https-https", + expectedResultSameOriginDownDown: SAME_ORIGIN + "rp=unsafe-url&upgrade=http-http", + expectedResultSameOriginUpDown: SAME_ORIGIN_HTTPS + "rp=unsafe-url&upgrade=https-http", + expectedResultCrossOriginDownDown:CROSS_ORIGIN + "rp=unsafe-url&upgrade=http-http", + expectedResultCrossOriginUpDown:CROSS_ORIGIN_HTTPS + "rp=unsafe-url&upgrade=https-http", + }, +]; + + +let currentTest = 0; +let sameOriginRequest = true; +let testWin; +let currentQuery; +window.addEventListener("message", receiveMessage); +let currentRun = 0; +// All combinations, HTTP -> HTTPS, HTTPS -> HTTPS, HTTP -> HTTP, HTTPS -> HTTP +const ALL_COMB = ["http-https", "https-https" ,"http-http", "https-http"]; + +// Receive message and verify that we receive the expected referrer header +async function receiveMessage(event) { + let data = event.data; + currentQuery = testCases[currentTest].query; + let currentComb = ALL_COMB[currentRun]; + // if request was http -> https + if (currentComb === "http-https") { + if (sameOriginRequest){ + is(data.result, testCases[currentTest].expectedResultSameOriginDownUp , + "We received for the downgraded same site request with referrer policy: " + currentQuery + " the correct referrer"); + is(data.location, SAME_ORIGIN_HTTPS + "sendMe","Opened correct location"); + } else { + is(data.result, testCases[currentTest].expectedResultCrossOriginDownUp , + "We received for the downgraded cross site request with referrer policy: " + currentQuery + " the correct referrer"); + is(data.location, SAME_ORIGIN_HTTPS + "sendMe", "Opened correct location"); + } + // if request was https -> https + } else if (currentComb === "https-https") { + if (sameOriginRequest){ + is(data.result, testCases[currentTest].expectedResultSameOriginUpUp , + "We received for the upgraded same site request with referrer policy: " + currentQuery + " the correct referrer"); + is(data.location, SAME_ORIGIN_HTTPS + "sendMe", "Opened correct location"); + } else { + is(data.result, testCases[currentTest].expectedResultCrossOriginUpUp, + "We received for the upgraded cross site request with referrer policy: " + currentQuery + " the correct referrer"); + is(data.location, SAME_ORIGIN_HTTPS + "sendMe", "Opened correct location"); + } + } else if (currentComb === "http-http") { + if (sameOriginRequest){ + is(data.result, testCases[currentTest].expectedResultSameOriginDownDown , + "We received for the upgraded same site request with referrer policy: " + currentQuery + " the correct referrer"); + is(data.location, SAME_ORIGIN + "sendMe2","Opened correct location for" + currentQuery + currentComb); + } else { + is(data.result, testCases[currentTest].expectedResultCrossOriginDownDown, + "We received for the upgraded cross site request with referrer policy: " + currentQuery + " the correct referrer"); + is(data.location, SAME_ORIGIN + "sendMe2", "Opened correct location " + currentQuery + currentComb); + } + } else if (currentComb === "https-http") { + if (sameOriginRequest){ + is(data.result, testCases[currentTest].expectedResultSameOriginUpDown , + "We received for the upgraded same site request with referrer policy: " + currentQuery + " the correct referrer"); + is(data.location, SAME_ORIGIN + "sendMe2","Opened correct location " + currentQuery + currentComb); + } else { + is(data.result, testCases[currentTest].expectedResultCrossOriginUpDown, + "We received for the upgraded cross site request with referrer policy: " + currentQuery + " the correct referrer"); + is(data.location, SAME_ORIGIN + "sendMe2", "Opened correct location " + currentQuery + currentComb); + } + } + testWin.close(); + currentRun++; + if (currentTest >= testCases.length -1 && currentRun === ALL_COMB.length && !sameOriginRequest) { + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); + return; + } + runTest(); +} + +async function runTest() { + currentQuery = testCases[currentTest].query; + // send same origin request + if (sameOriginRequest && currentRun < ALL_COMB.length) { + // if upgrade = 0 downgrade request, else upgrade + testWin = window.open(SAME_ORIGIN + "rp=" +currentQuery + "&upgrade=" + ALL_COMB[currentRun], "_blank"); + } else { + // if same origin isn't set, check if we need to send cross origin requests + // eslint-disable-next-line no-lonely-if + if (!sameOriginRequest && currentRun < ALL_COMB.length ) { + // if upgrade = 0 downgrade request, else upgrade + testWin = window.open(CROSS_ORIGIN + "rp=" +currentQuery + "&upgrade=" + ALL_COMB[currentRun], "_blank"); + } // else we completed all test case of the current query for the current origin. Prepare and call next test + else { + // reset currentRun and go to next query + currentRun = 0; + if(!sameOriginRequest){ + currentTest++; + } + // run same test again for crossOrigin or start new test with sameOrigin + sameOriginRequest = !sameOriginRequest; + currentQuery = testCases[currentTest].query; + runTest(); + } + } +} + +SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_first", true], + ["network.http.referer.disallowCrossSiteRelaxingDefault", false], + ]}, runTest); + +</script> +</body> +</html> diff --git a/dom/security/test/https-first/test_resource_upgrade.html b/dom/security/test/https-first/test_resource_upgrade.html new file mode 100644 index 0000000000..c71879ac42 --- /dev/null +++ b/dom/security/test/https-first/test_resource_upgrade.html @@ -0,0 +1,118 @@ +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>HTTPS-First Mode - Resource Upgrade</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + <h1>HTTPS-First Mode</h1> + <p>Upgrade Test for various resources</p> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1704454">Bug 1704454/a> + <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 they do not get upgraded to *https* because + * https-first only applies to top-level requests. + * + * 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 *http* requests. + */ + + const { AppConstants } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + const splitRegex = /^(.*)-(.*)$/ + const testConfig = { + topLevelScheme: "http://", + results: [ + "iframe", "script", "img", "img-redir", "font", "xhr", "style", + "media", "object", "form", "nested-img","top-level" + ] + } + + + function runTest() { + // 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) { + checkResult(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(() => { + var src = testConfig.topLevelScheme + "example.com/tests/dom/security/test/https-first/file_upgrade_insecure.html"; + document.getElementById("testframe").src = src; + }); + } + + // 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); + function receiveMessage(event) { + checkResult(event.data.result); + } + + function finishTest() { + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); + } + + function checkResult(response) { + // A response looks either like this "iframe-ok" or "[key]-[result]" + const [, key, result] = splitRegex.exec(response) + // try to find the expected result within the results array + var index = testConfig.results.indexOf(key); + + // If the response is not even part of the results array, something is super wrong + if (index == -1) { + ok(false, `Unexpected response from server (${response})`); + finishTest(); + } + + // take the element out the array and continue till the results array is empty + if (index != -1) { + testConfig.results.splice(index, 1); + } + + // Check if the result was okay or had an error + is(result, 'ok', `Upgrade all requests on toplevel http for '${key}' came back with: '${result}'`) + + // If we're not expecting any more resulsts, finish the test + if (!testConfig.results.length) { + finishTest(); + } + } + + SimpleTest.waitForExplicitFinish(); + // Set preference and start test + SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_first", true], + ["security.mixed_content.block_active_content", false], + ["security.mixed_content.block_display_content", false] + ] }, runTest); + + </script> +</body> + +</html> diff --git a/dom/security/test/https-first/test_toplevel_cookies.html b/dom/security/test/https-first/test_toplevel_cookies.html new file mode 100644 index 0000000000..2c0c64db46 --- /dev/null +++ b/dom/security/test/https-first/test_toplevel_cookies.html @@ -0,0 +1,116 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1711453 : HTTPS-First: Add test for cookies </title> +<script 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"> +"use strict"; +/* + * Description of the test: + * We perform each test with 4 different cookie settings and + * expect https-first to detect which cookie is same origin and + * which is cross origin. The cookies are in an image or in a frame. + * The 4 cookie settings differ in two flags which are set or not. + * The first call is always with secure flag not set and sameSite=none + * In the second call we don't set the secure flag but sameSite=strict + * In the third call we set the secure flag and sameSite=none + * In the forth call we set the secure flag and sameSite=strict + * More detailed: + * We run the tests in the following order. + * Test 1a: Image is loaded with cookie same-origin, not secure and sameSite=none + * Test 1b: Image is loaded with cookie same-origin, not secure and sameSite=strict + * Test 1c: Image is loaded with cookie same-origin, secure and sameSite=none + * Test 1d: Image is loaded with cookie same-origin, secure and sameSite=strict + * Test 1e: Image is loaded with cookie cross-origin, not secure and sameSite=none + * Test 1f: Image is loaded with cookie cross-origin, not secure and sameSite=strict + * Test 2a: Load frame navigation with cookie same-origin, not secure and sameSite=none + * ... + * Test 3a: Load frame navigation blank with cookie same-origin, not secure and sameSite=none + * ... + * Test 4a: Load frame Inc with cookie same-origin, not secure and sameSite=none + * ... + * Test 5a: Load frame Inc Blank with cookie same-origin, not secure and sameSite=none + * ... + */ + +SimpleTest.waitForExplicitFinish(); + +const SAME_ORIGIN = + "http://example.com/tests/dom/security/test/https-first/file_toplevel_cookies.sjs?"; + +const CROSS_ORIGIN = + "http://example.org/tests/dom/security/test/https-first/file_toplevel_cookies.sjs?"; + +const redirectQueries = ["setImage", "loadNav", "loadNavBlank","loadframeInc", "loadframeIncBlank"]; +let currentTest = 0; +let sameOriginRequest = true; +let testWin; +let currentQuery; +window.addEventListener("message", receiveMessage); +let currentRun = 0; +// All possible cookie attribute combinations +// cookie attributes are secure=set/not set and sameSite= none/ strict +const ALL_COOKIE_COMB = ["notSecure,none", "notSecure,strict", "secure,none", "secure,strict"] + +// Receive message and verify that it is from an https site. +// When the message is 'upgraded' then it was send by an https site +// and validate that we received the right cookie. Verify that for a cross +//origin request we didn't receive a cookie. +async function receiveMessage(event) { + let data = event.data; + currentQuery = redirectQueries[currentTest]; + ok(data.result === "upgraded", "Upgraded successful to https for " + currentQuery); + ok(data.loc.includes("https"), "scheme is 'https' for " + currentQuery ); + if (!sameOriginRequest) { + ok(data.cookie === "", "Cookie from cross-Origin site shouldn't be accepted " + currentQuery + " " + ALL_COOKIE_COMB[currentRun]); + } else { + is(data.cookie.includes(currentQuery + "=" + currentRun), true, "Cookie successfully arrived for " + currentQuery + " " + ALL_COOKIE_COMB[currentRun]); + } + testWin.close(); + currentRun++; + if (currentTest >= redirectQueries.length -1 && currentRun === ALL_COOKIE_COMB.length && !sameOriginRequest) { + window.removeEventListener("message", receiveMessage); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + SimpleTest.finish(); + return; + } + runTest(); +} + +async function runTest() { + currentQuery = redirectQueries[currentTest]; + // send same origin request + if (sameOriginRequest && currentRun < ALL_COOKIE_COMB.length) { + testWin = window.open(SAME_ORIGIN + currentQuery + currentRun, "_blank"); + } else { + // if same origin isn't set, check if we need to send cross origin requests + // eslint-disable-next-line no-lonely-if + if (!sameOriginRequest && currentRun < ALL_COOKIE_COMB.length ) { + testWin = window.open(CROSS_ORIGIN + currentQuery + currentRun, "_blank"); + } // else we completed all test case of the current query for the current origin. Prepare and call next test + else { + // reset currentRun and go to next query + currentRun = 0; + if(!sameOriginRequest){ + currentTest++; + } + // run same test again for crossOrigin or start new test with sameOrigin + sameOriginRequest = !sameOriginRequest; + currentQuery = redirectQueries[currentTest]; + runTest(); + } + } +} + +SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_first", true], + ["network.cookie.sameSite.noneRequiresSecure", false], + ]}, runTest); + +</script> +</body> +</html> diff --git a/dom/security/test/https-only/browser.ini b/dom/security/test/https-only/browser.ini new file mode 100644 index 0000000000..e163791023 --- /dev/null +++ b/dom/security/test/https-only/browser.ini @@ -0,0 +1,36 @@ +[DEFAULT] +prefs = + dom.security.https_first=false + +[browser_console_logging.js] +support-files = + file_console_logging.html +[browser_upgrade_exceptions.js] +[browser_httpsonly_prefs.js] +[browser_cors_mixedcontent.js] +support-files = + file_cors_mixedcontent.html +[browser_iframe_test.js] +skip-if = + os == 'linux' && bits == 64 # Bug 1735565 + os == 'win' && bits == 64 # Bug 1735565 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +support-files = + file_iframe_test.sjs +[browser_triggering_principal_exemption.js] +[browser_background_redirect.js] +support-files = + file_background_redirect.sjs +[browser_user_gesture.js] +support-files = + file_user_gesture.html +[browser_hsts_host.js] +support-files = + hsts_headers.sjs +[browser_httpsonly_speculative_connect.js] +support-files = file_httpsonly_speculative_connect.html +[browser_websocket_exceptions.js] +skip-if = (toolkit == 'android') # WebSocket tests are not supported on Android Yet. Bug 1566168. +support-files = + file_websocket_exceptions.html + file_websocket_exceptions_iframe.html diff --git a/dom/security/test/https-only/browser_background_redirect.js b/dom/security/test/https-only/browser_background_redirect.js new file mode 100644 index 0000000000..6585ef0faf --- /dev/null +++ b/dom/security/test/https-only/browser_background_redirect.js @@ -0,0 +1,64 @@ +"use strict"; +/* Description of the test: + * We load a page which gets upgraded to https. HTTPS-Only Mode then + * sends an 'http' background request which we redirect (using the + * web extension API) to 'same-origin' https. We ensure the HTTPS-Only + * Error page does not occur, but we https page gets loaded. + */ + +let extension = null; + +add_task(async function test_https_only_background_request_redirect() { + // A longer timeout is necessary for this test since we have to wait + // at least 3 seconds for the https-only background request to happen. + requestLongerTimeout(10); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + + extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "webRequestBlocking", "http://example.com/*"], + }, + background() { + let { browser } = this; + browser.webRequest.onBeforeRequest.addListener( + details => { + if (details.url === "http://example.com/") { + browser.test.sendMessage("redir-handled"); + let redirectUrl = "https://example.com/"; + return { redirectUrl }; + } + return undefined; + }, + { urls: ["http://example.com/*"] }, + ["blocking"] + ); + }, + }); + + await extension.startup(); + + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + let loaded = BrowserTestUtils.browserLoaded(browser, false, null, true); + BrowserTestUtils.loadURI( + browser, + "http://example.com/browser/dom/security/test/https-only/file_background_redirect.sjs?start" + ); + + await extension.awaitMessage("redir-handled"); + + await loaded; + + await SpecialPowers.spawn(browser, [], async function() { + let innerHTML = content.document.body.innerHTML; + ok( + innerHTML.includes("Test Page for Bug 1683015 loaded"), + "No https-only error page" + ); + }); + }); + + await extension.unload(); +}); diff --git a/dom/security/test/https-only/browser_console_logging.js b/dom/security/test/https-only/browser_console_logging.js new file mode 100644 index 0000000000..473d2ec2f5 --- /dev/null +++ b/dom/security/test/https-only/browser_console_logging.js @@ -0,0 +1,150 @@ +// Bug 1625448 - HTTPS Only Mode - Tests for console logging +// https://bugzilla.mozilla.org/show_bug.cgi?id=1625448 +// This test makes sure that the various console messages from the HTTPS-Only +// mode get logged to the console. +"use strict"; + +// Test Cases +// description: Description of what the subtests expects. +// expectLogLevel: Expected log-level of a message. +// expectIncludes: Expected substrings the message should contain. +let tests = [ + { + description: "Top-Level upgrade should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: [ + "HTTPS-Only Mode: Upgrading insecure request", + "to use", + "file_console_logging.html", + ], + }, + { + description: "iFrame upgrade failure should get logged", + expectLogLevel: Ci.nsIConsoleMessage.error, + expectIncludes: [ + "HTTPS-Only Mode: Upgrading insecure request", + "failed", + "file_console_logging.html", + ], + }, + { + description: "WebSocket upgrade should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: [ + "HTTPS-Only Mode: Upgrading insecure request", + "to use", + "ws://does.not.exist", + ], + }, + { + description: "Sub-Resource upgrade for file_1 should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: ["Upgrading insecure", "request", "file_1.jpg"], + }, + { + description: "Sub-Resource upgrade for file_2 should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: ["Upgrading insecure", "request", "to use", "file_2.jpg"], + }, + { + description: "Exempt request for file_exempt should get logged", + expectLogLevel: Ci.nsIConsoleMessage.info, + expectIncludes: [ + "Not upgrading insecure request", + "because it is exempt", + "file_exempt.jpg", + ], + }, + { + description: "Sub-Resource upgrade failure for file_2 should get logged", + expectLogLevel: Ci.nsIConsoleMessage.error, + expectIncludes: ["Upgrading insecure request", "failed", "file_2.jpg"], + }, +]; + +const testPathUpgradeable = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); +// DNS errors are not logged as HTTPS-Only Mode upgrade failures, so we have to +// upgrade to a domain that exists but fails. +const testPathNotUpgradeable = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://self-signed.example.com" +); +const kTestURISuccess = testPathUpgradeable + "file_console_logging.html"; +const kTestURIFail = testPathNotUpgradeable + "file_console_logging.html"; +const kTestURIExempt = testPathUpgradeable + "file_exempt.jpg"; + +const UPGRADE_DISPLAY_CONTENT = + "security.mixed_content.upgrade_display_content"; + +add_task(async function() { + // A longer timeout is necessary for this test than the plain mochitests + // due to opening a new tab with the web console. + requestLongerTimeout(4); + + // Enable HTTPS-Only Mode and register console-listener + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + Services.console.registerListener(on_new_message); + // 1. Upgrade page to https:// + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, kTestURISuccess); + // 2. Make an exempt http:// request + let xhr = new XMLHttpRequest(); + xhr.open("GET", kTestURIExempt, true); + xhr.channel.loadInfo.httpsOnlyStatus |= Ci.nsILoadInfo.HTTPS_ONLY_EXEMPT; + xhr.send(); + // 3. Make Websocket request + new WebSocket("ws://does.not.exist"); + + await BrowserTestUtils.waitForCondition(() => tests.length === 0); + + // Clean up + Services.console.unregisterListener(on_new_message); +}); + +function on_new_message(msgObj) { + const message = msgObj.message; + const logLevel = msgObj.logLevel; + + // Bools about message and pref + const isMCL2Enabled = Services.prefs.getBoolPref(UPGRADE_DISPLAY_CONTENT); + const isHTTPSOnlyModeLog = message.includes("HTTPS-Only Mode:"); + const isMCLog = message.includes("Mixed Content:"); + + // Check for messages about HTTPS-only upgrades (those should be unrelated to mixed content upgrades) + // or for mixed content upgrades which should only occur if security.mixed_content.upgrade_display_content is enabled + // (unrelated to https-only logs). + if ( + (isHTTPSOnlyModeLog && !isMCLog) || + (isMCLog && isMCL2Enabled && !isHTTPSOnlyModeLog) + ) { + for (let i = 0; i < tests.length; i++) { + const testCase = tests[i]; + // If security.mixed_content.upgrade_display_content is enabled, the mixed content control mechanism is upgrading file2.jpg + // and HTTPS-Only mode is not failing upgrading file2.jpg, so it won't be logged. + // so skip last test case + if ( + testCase.description == + "Sub-Resource upgrade failure for file_2 should get logged" && + isMCL2Enabled + ) { + tests.splice(i, 1); + continue; + } + // Check if log-level matches + if (logLevel !== testCase.expectLogLevel) { + continue; + } + // Check if all substrings are included + if (testCase.expectIncludes.some(str => !message.includes(str))) { + continue; + } + ok(true, testCase.description); + tests.splice(i, 1); + break; + } + } +} diff --git a/dom/security/test/https-only/browser_cors_mixedcontent.js b/dom/security/test/https-only/browser_cors_mixedcontent.js new file mode 100644 index 0000000000..6b3059aebe --- /dev/null +++ b/dom/security/test/https-only/browser_cors_mixedcontent.js @@ -0,0 +1,124 @@ +// Bug 1659505 - Https-Only: CORS and MixedContent tests +// https://bugzilla.mozilla.org/bug/1659505 +"use strict"; + +// > How does this test work? +// We open a page, that makes two fetch-requests to example.com (same-origin) +// and example.org (cross-origin). When both fetch-calls have either failed or +// succeeded, the site dispatches an event with the results. + +add_task(async function() { + // HTTPS-Only Mode disabled + await runTest({ + description: "Load site with HTTP and HOM disabled", + topLevelScheme: "http", + + expectedSameOrigin: "success", // ok + expectedCrossOrigin: "error", // CORS + }); + await runTest({ + description: "Load site with HTTPS and HOM disabled", + topLevelScheme: "https", + + expectedSameOrigin: "error", // Mixed Content + expectedCrossOrigin: "error", // Mixed Content + }); + + // HTTPS-Only Mode disabled and MixedContent blocker disabled + await SpecialPowers.pushPrefEnv({ + set: [["security.mixed_content.block_active_content", false]], + }); + await runTest({ + description: "Load site with HTTPS; HOM and MixedContent blocker disabled", + topLevelScheme: "https", + + expectedSameOrigin: "error", // CORS + expectedCrossOrigin: "error", // CORS + }); + await SpecialPowers.popPrefEnv(); + + // HTTPS-Only Mode enabled, no exception + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + await runTest({ + description: "Load site with HTTP and HOM enabled", + topLevelScheme: "http", + + expectedSameOrigin: "success", // ok + expectedCrossOrigin: "error", // CORS + }); + + // HTTPS-Only enabled, with exception + await SpecialPowers.pushPermissions([ + { + type: "https-only-load-insecure", + allow: true, + context: "http://example.com", + }, + ]); + + await runTest({ + description: "Load site with HTTP, HOM enabled but site exempt", + topLevelScheme: "http", + + expectedSameOrigin: "success", // ok + expectedCrossOrigin: "error", // CORS + }); + + await SpecialPowers.popPermissions(); + await SpecialPowers.pushPermissions([ + { + type: "https-only-load-insecure", + allow: true, + context: "https://example.com", + }, + ]); + await runTest({ + description: "Load site with HTTPS, HOM enabled but site exempt", + topLevelScheme: "https", + + expectedSameOrigin: "error", // Mixed Content + expectedCrossOrigin: "error", // Mixed Content + }); + + // Remove permission again (has to be done manually for some reason?) + await SpecialPowers.popPermissions(); +}); + +const SERVER_URL = scheme => + `${scheme}://example.com/browser/dom/security/test/https-only/file_cors_mixedcontent.html`; + +async function runTest(test) { + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + let loaded = BrowserTestUtils.browserLoaded(browser); + + BrowserTestUtils.loadURI(browser, SERVER_URL(test.topLevelScheme)); + + await loaded; + + // eslint-disable-next-line no-shadow + await SpecialPowers.spawn(browser, [test], async function(test) { + const promise = new Promise(resolve => { + content.addEventListener("FetchEnded", resolve, { + once: true, + }); + }); + + content.dispatchEvent(new content.Event("StartFetch")); + + const { detail } = await promise; + + is( + detail.comResult, + test.expectedSameOrigin, + `${test.description} (same-origin)` + ); + is( + detail.orgResult, + test.expectedCrossOrigin, + `${test.description} (cross-origin)` + ); + }); + }); +} diff --git a/dom/security/test/https-only/browser_hsts_host.js b/dom/security/test/https-only/browser_hsts_host.js new file mode 100644 index 0000000000..67c8cf6601 --- /dev/null +++ b/dom/security/test/https-only/browser_hsts_host.js @@ -0,0 +1,123 @@ +// Bug 1722489 - HTTPS-Only Mode - Tests evaluation order +// https://bugzilla.mozilla.org/show_bug.cgi?id=1722489 +// This test ensures that an http request to an hsts host +// gets upgraded by hsts and not by https-only. +"use strict"; + +// Set bools to track that tests ended. +let readMessage = false; +let testFinished = false; +// Visit a secure site that sends an HSTS header to set up the rest of the +// test. +add_task(async function see_hsts_header() { + let setHstsUrl = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "hsts_headers.sjs"; + Services.obs.addObserver(observer, "http-on-examine-response"); + await BrowserTestUtils.loadURI(gBrowser.selectedBrowser, setHstsUrl); + + await BrowserTestUtils.waitForCondition(() => readMessage); + // Clean up + Services.obs.removeObserver(observer, "http-on-examine-response"); +}); + +// Test that HTTPS_Only is not performed if HSTS host is visited. +add_task(async function() { + // A longer timeout is necessary for this test than the plain mochitests + // due to opening a new tab with the web console. + requestLongerTimeout(4); + + // Enable HTTPS-Only Mode and register console-listener + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + Services.console.registerListener(onNewMessage); + const RESOURCE_LINK = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" + ) + "hsts_headers.sjs"; + + // 1. Upgrade page to https:// + await BrowserTestUtils.loadURI(gBrowser.selectedBrowser, RESOURCE_LINK); + + await BrowserTestUtils.waitForCondition(() => testFinished); + + // Clean up + Services.console.unregisterListener(onNewMessage); +}); + +add_task(async function() { + // Reset HSTS header + readMessage = false; + let clearHstsUrl = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "hsts_headers.sjs?reset"; + + Services.obs.addObserver(observer, "http-on-examine-response"); + // reset hsts header + await BrowserTestUtils.loadURI(gBrowser.selectedBrowser, clearHstsUrl); + await BrowserTestUtils.waitForCondition(() => readMessage); + // Clean up + Services.obs.removeObserver(observer, "http-on-examine-response"); +}); + +function observer(subject, topic, state) { + info("observer called with " + topic); + if (topic == "http-on-examine-response") { + onExamineResponse(subject); + } +} + +function onExamineResponse(subject) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + // If message was already read or is not related to "example.com", + // don't examine it. + if (!channel.URI.spec.includes("example.com") || readMessage) { + return; + } + info("onExamineResponse with " + channel.URI.spec); + if (channel.URI.spec.includes("reset")) { + try { + let hsts = channel.getResponseHeader("Strict-Transport-Security"); + is(hsts, "max-age=0", "HSTS header is not set"); + } catch (e) { + ok(false, "HSTS header still set"); + } + readMessage = true; + return; + } + try { + let hsts = channel.getResponseHeader("Strict-Transport-Security"); + let csp = channel.getResponseHeader("Content-Security-Policy"); + // Check that HSTS and CSP upgrade headers are set + is(hsts, "max-age=60", "HSTS header is set"); + is(csp, "upgrade-insecure-requests", "CSP header is set"); + } catch (e) { + ok(false, "No header set"); + } + readMessage = true; +} + +function onNewMessage(msgObj) { + const message = msgObj.message; + // ensure that request is not upgraded HTTPS-Only. + if (message.includes("Upgrading insecure request")) { + ok(false, "Top-Level upgrade shouldn't get logged"); + testFinished = true; + } else if ( + message.includes("Upgrading insecure speculative TCP connection") + ) { + // TODO: Check assertion + // https://bugzilla.mozilla.org/show_bug.cgi?id=1735683 + ok(true, "Top-Level upgrade shouldn't get logged"); + testFinished = true; + } else if (gBrowser.selectedBrowser.currentURI.scheme === "https") { + ok(true, "Top-Level upgrade shouldn't get logged"); + testFinished = true; + } +} diff --git a/dom/security/test/https-only/browser_httpsonly_prefs.js b/dom/security/test/https-only/browser_httpsonly_prefs.js new file mode 100644 index 0000000000..e6ae06bb29 --- /dev/null +++ b/dom/security/test/https-only/browser_httpsonly_prefs.js @@ -0,0 +1,118 @@ +"use strict"; + +async function runPrefTest( + aHTTPSOnlyPref, + aHTTPSOnlyPrefPBM, + aExecuteFromPBM, + aDesc, + aAssertURLStartsWith +) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_only_mode", aHTTPSOnlyPref], + ["dom.security.https_only_mode_pbm", aHTTPSOnlyPrefPBM], + ], + }); + + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + await ContentTask.spawn( + browser, + { aExecuteFromPBM, aDesc, aAssertURLStartsWith }, + // eslint-disable-next-line no-shadow + async function({ aExecuteFromPBM, aDesc, aAssertURLStartsWith }) { + const responseURL = await new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.timeout = 1200; + xhr.open("GET", "http://example.com"); + if (aExecuteFromPBM) { + xhr.channel.loadInfo.originAttributes = { + privateBrowsingId: 1, + }; + } + xhr.onreadystatechange = () => { + // We don't care about the result and it's possible that + // the requests might even succeed in some testing environments + if ( + xhr.readyState !== XMLHttpRequest.OPENED || + xhr.readyState !== XMLHttpRequest.UNSENT + ) { + // Let's make sure this function does not get called anymore + xhr.onreadystatechange = undefined; + resolve(xhr.responseURL); + } + }; + xhr.send(); + }); + ok(responseURL.startsWith(aAssertURLStartsWith), aDesc); + } + ); + }); +} + +add_task(async function() { + requestLongerTimeout(2); + + await runPrefTest( + false, + false, + false, + "Setting no prefs should not upgrade", + "http://" + ); + + await runPrefTest( + true, + false, + false, + "Setting aHTTPSOnlyPref should upgrade", + "https://" + ); + + await runPrefTest( + false, + true, + false, + "Setting aHTTPSOnlyPrefPBM should not upgrade", + "http://" + ); + + await runPrefTest( + false, + false, + true, + "Setting aPBM should not upgrade", + "http://" + ); + + await runPrefTest( + true, + true, + false, + "Setting aHTTPSOnlyPref and aHTTPSOnlyPrefPBM should should upgrade", + "https://" + ); + + await runPrefTest( + true, + false, + true, + "Setting aHTTPSOnlyPref and aPBM should upgrade", + "https://" + ); + + await runPrefTest( + false, + true, + true, + "Setting aHTTPSOnlyPrefPBM and aPBM should upgrade", + "https://" + ); + + await runPrefTest( + true, + true, + true, + "Setting aHTTPSOnlyPref and aHTTPSOnlyPrefPBM and aPBM should upgrade", + "https://" + ); +}); diff --git a/dom/security/test/https-only/browser_httpsonly_speculative_connect.js b/dom/security/test/https-only/browser_httpsonly_speculative_connect.js new file mode 100644 index 0000000000..d07f335b99 --- /dev/null +++ b/dom/security/test/https-only/browser_httpsonly_speculative_connect.js @@ -0,0 +1,69 @@ +"use strict"; + +const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.org" +); + +let console_messages = [ + { + description: "Speculative Connection should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: [ + "Upgrading insecure speculative TCP connection", + "to use", + "example.org", + "file_httpsonly_speculative_connect.html", + ], + }, + { + description: "Upgrade should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: [ + "Upgrading insecure request", + "to use", + "example.org", + "file_httpsonly_speculative_connect.html", + ], + }, +]; + +function on_new_console_messages(msgObj) { + const message = msgObj.message; + const logLevel = msgObj.logLevel; + + if (message.includes("HTTPS-Only Mode:")) { + for (let i = 0; i < console_messages.length; i++) { + const testCase = console_messages[i]; + // Check if log-level matches + if (logLevel !== testCase.expectLogLevel) { + continue; + } + // Check if all substrings are included + if (testCase.expectIncludes.some(str => !message.includes(str))) { + continue; + } + ok(true, testCase.description); + console_messages.splice(i, 1); + break; + } + } +} + +add_task(async function() { + requestLongerTimeout(4); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + Services.console.registerListener(on_new_console_messages); + + await BrowserTestUtils.loadURI( + gBrowser.selectedBrowser, + `${TEST_PATH_HTTP}file_httpsonly_speculative_connect.html` + ); + + await BrowserTestUtils.waitForCondition(() => console_messages.length === 0); + + Services.console.unregisterListener(on_new_console_messages); +}); diff --git a/dom/security/test/https-only/browser_iframe_test.js b/dom/security/test/https-only/browser_iframe_test.js new file mode 100644 index 0000000000..67a3974c96 --- /dev/null +++ b/dom/security/test/https-only/browser_iframe_test.js @@ -0,0 +1,223 @@ +// Bug 1658264 - Https-Only: HTTPS-Only and iFrames +// https://bugzilla.mozilla.org/show_bug.cgi?id=1658264 +"use strict"; + +// > How does this test work? +// We're sending a request to file_iframe_test.sjs with various +// browser-configurations. The sjs-file returns a website with two iFrames +// loading the same sjs-file again. One iFrame is same origin (example.com) and +// the other cross-origin (example.org) Each request gets saved in a semicolon +// seperated list of strings. The sjs-file gets initialized with the +// query-string "setup" and the result string can be polled with "results". Each +// string has this format: {top/com/org}-{queryString}-{scheme}. In the end +// we're just checking if all expected requests were recorded and had the +// correct scheme. Requests that are meant to fail should explicitly not be +// contained in the list of results. + +// The test loads all tabs and evaluates when all have finished loading +// it may take quite a long time. +// This requires more twice as much as the default 45 seconds per test: +requestLongerTimeout(2); +SimpleTest.requestCompleteLog(); + +add_task(async function() { + await setup(); + + // Using this variable to parallelize and collect tests + let testSet = []; + + /* + * HTTPS-Only Mode disabled + */ + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", false]], + }); + + // Top-Level scheme: HTTP + // NOTE(freddyb): Test case temporarily disabled. See bug 1735565 + /*testSet.push( + runTest({ + queryString: "test1.1", + topLevelScheme: "http", + + expectedTopLevel: "http", + expectedSameOrigin: "http", + expectedCrossOrigin: "http", + }) + );*/ + // Top-Level scheme: HTTPS + testSet.push( + runTest({ + queryString: "test1.2", + topLevelScheme: "https", + + expectedTopLevel: "https", + expectedSameOrigin: "fail", + expectedCrossOrigin: "fail", + }) + ); + + await Promise.all(testSet); + testSet = []; + /* + * HTTPS-Only Mode enabled, no exception + */ + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + + // Top-Level scheme: HTTP + testSet.push( + runTest({ + queryString: "test2.1", + topLevelScheme: "http", + + expectedTopLevel: "https", + expectedSameOrigin: "https", + expectedCrossOrigin: "https", + }) + ); + // Top-Level scheme: HTTPS + testSet.push( + runTest({ + queryString: "test2.2", + topLevelScheme: "https", + + expectedTopLevel: "https", + expectedSameOrigin: "https", + expectedCrossOrigin: "https", + }) + ); + + await Promise.all(testSet); + testSet = []; + + /* + * HTTPS-Only enabled, with exceptions + * for http://example.org and http://example.com + */ + // Exempting example.org (cross-site) should not affect anything + await SpecialPowers.pushPermissions([ + { + type: "https-only-load-insecure", + allow: true, + context: "http://example.org", + }, + ]); + await SpecialPowers.pushPermissions([ + { + type: "https-only-load-insecure", + allow: true, + context: "http://example.com", + }, + ]); + + // Top-Level scheme: HTTP + await runTest({ + queryString: "test3.1", + topLevelScheme: "http", + + expectedTopLevel: "http", + expectedSameOrigin: "http", + expectedCrossOrigin: "http", + }); + + await SpecialPowers.popPermissions(); + await SpecialPowers.pushPermissions([ + { + type: "https-only-load-insecure", + allow: true, + context: "https://example.com", + }, + ]); + // Top-Level scheme: HTTPS + await runTest({ + queryString: "test3.2", + topLevelScheme: "https", + + expectedTopLevel: "https", + expectedSameOrigin: "fail", + expectedCrossOrigin: "fail", + }); + + // Remove permissions again (has to be done manually for some reason?) + await SpecialPowers.popPermissions(); + await SpecialPowers.popPermissions(); + + await evaluate(); +}); + +const SERVER_URL = scheme => + `${scheme}://example.com/browser/dom/security/test/https-only/file_iframe_test.sjs?`; +let shouldContain = []; +let shouldNotContain = []; + +async function setup() { + info(`TEST-CASE-setup - A`); + const response = await fetch(SERVER_URL("https") + "setup"); + info(`TEST-CASE-setup - B`); + const txt = await response.text(); + info(`TEST-CASE-setup - C`); + if (txt != "ok") { + ok(false, "Failed to setup test server."); + finish(); + } +} + +async function evaluate() { + info(`TEST-CASE-evaluate - A`); + const response = await fetch(SERVER_URL("https") + "results"); + info(`TEST-CASE-evaluate - B`); + const requestResults = (await response.text()).split(";"); + info(`TEST-CASE-evaluate - C`); + + shouldContain.map(str => + ok(requestResults.includes(str), `Results should contain '${str}'.`) + ); + shouldNotContain.map(str => + ok(!requestResults.includes(str), `Results shouldn't contain '${str}'.`) + ); +} + +async function runTest(test) { + const queryString = test.queryString; + info(`TEST-CASE-${test.queryString} - runTest BEGIN`); + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + let loaded = BrowserTestUtils.browserLoaded( + browser, + false, // includeSubFrames + SERVER_URL(test.expectedTopLevel) + queryString, + false // maybeErrorPage + ); + BrowserTestUtils.loadURI( + browser, + SERVER_URL(test.topLevelScheme) + queryString + ); + info(`TEST-CASE-${test.queryString} - Before 'await loaded'`); + await loaded; + info(`TEST-CASE-${test.queryString} - After 'await loaded'`); + }); + info(`TEST-CASE-${test.queryString} - After 'await withNewTab'`); + + if (test.expectedTopLevel !== "fail") { + shouldContain.push(`top-${queryString}-${test.expectedTopLevel}`); + } else { + shouldNotContain.push(`top-${queryString}-http`); + shouldNotContain.push(`top-${queryString}-https`); + } + + if (test.expectedSameOrigin !== "fail") { + shouldContain.push(`com-${queryString}-${test.expectedSameOrigin}`); + } else { + shouldNotContain.push(`com-${queryString}-http`); + shouldNotContain.push(`com-${queryString}-https`); + } + + if (test.expectedCrossOrigin !== "fail") { + shouldContain.push(`org-${queryString}-${test.expectedCrossOrigin}`); + } else { + shouldNotContain.push(`org-${queryString}-http`); + shouldNotContain.push(`org-${queryString}-https`); + } + info(`TEST-CASE-${test.queryString} - runTest END`); +} diff --git a/dom/security/test/https-only/browser_triggering_principal_exemption.js b/dom/security/test/https-only/browser_triggering_principal_exemption.js new file mode 100644 index 0000000000..d2d4038ac8 --- /dev/null +++ b/dom/security/test/https-only/browser_triggering_principal_exemption.js @@ -0,0 +1,71 @@ +// Bug 1662359 - Don't upgrade subresources whose triggering principal is exempt from HTTPS-Only mode. +// https://bugzilla.mozilla.org/bug/1662359 +"use strict"; + +const TRIGGERING_PAGE = "http://example.org"; +const LOADED_RESOURCE = "http://example.com"; + +add_task(async function() { + // Enable HTTPS-Only Mode + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + + await runTest( + "Request with not exempt triggering principal should get upgraded.", + "https://" + ); + + // Now exempt the triggering page + await SpecialPowers.pushPermissions([ + { + type: "https-only-load-insecure", + allow: true, + context: TRIGGERING_PAGE, + }, + ]); + + await runTest( + "Request with exempt triggering principal should not get upgraded.", + "http://" + ); + + await SpecialPowers.popPermissions(); +}); + +async function runTest(desc, startsWith) { + const responseURL = await new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.open("GET", LOADED_RESOURCE); + + // Replace loadinfo with one whose triggeringPrincipal is a content + // principal for TRIGGERING_PAGE. + const triggeringPrincipal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + TRIGGERING_PAGE + ); + let dummyURI = Services.io.newURI(LOADED_RESOURCE); + let dummyChannel = NetUtil.newChannel({ + uri: dummyURI, + triggeringPrincipal, + loadingPrincipal: xhr.channel.loadInfo.loadingPrincipal, + securityFlags: xhr.channel.loadInfo.securityFlags, + contentPolicyType: xhr.channel.loadInfo.externalContentPolicyType, + }); + xhr.channel.loadInfo = dummyChannel.loadInfo; + + xhr.onreadystatechange = () => { + // We don't care about the result, just if Firefox upgraded the URL + // internally. + if ( + xhr.readyState !== XMLHttpRequest.OPENED || + xhr.readyState !== XMLHttpRequest.UNSENT + ) { + // Let's make sure this function doesn't get called anymore + xhr.onreadystatechange = undefined; + resolve(xhr.responseURL); + } + }; + xhr.send(); + }); + ok(responseURL.startsWith(startsWith), desc); +} diff --git a/dom/security/test/https-only/browser_upgrade_exceptions.js b/dom/security/test/https-only/browser_upgrade_exceptions.js new file mode 100644 index 0000000000..d1916b3552 --- /dev/null +++ b/dom/security/test/https-only/browser_upgrade_exceptions.js @@ -0,0 +1,86 @@ +// Bug 1625448 - HTTPS Only Mode - Exceptions for loopback and local IP addresses +// https://bugzilla.mozilla.org/show_bug.cgi?id=1631384 +// This test ensures that various configurable upgrade exceptions work +"use strict"; + +add_task(async function() { + requestLongerTimeout(2); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + + // Loopback test + await runTest( + "Loopback IP addresses should always be exempt from upgrades (localhost)", + "http://localhost", + "http://" + ); + await runTest( + "Loopback IP addresses should always be exempt from upgrades (127.0.0.1)", + "http://127.0.0.1", + "http://" + ); + // Default local-IP and onion tests + await runTest( + "Local IP addresses should be exempt from upgrades by default", + "http://10.0.250.250", + "http://" + ); + await runTest( + "Hosts ending with .onion should be be exempt from HTTPS-Only upgrades by default", + "http://grocery.shopping.for.one.onion", + "http://" + ); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_only_mode.upgrade_local", true], + ["dom.security.https_only_mode.upgrade_onion", true], + ], + }); + + // Local-IP and onion tests with upgrade enabled + await runTest( + "Local IP addresses should get upgraded when 'dom.security.https_only_mode.upgrade_local' is set to true", + "http://10.0.250.250", + "https://" + ); + await runTest( + "Hosts ending with .onion should get upgraded when 'dom.security.https_only_mode.upgrade_onion' is set to true", + "http://grocery.shopping.for.one.onion", + "https://" + ); + // Local-IP request with HTTPS_ONLY_EXEMPT flag + await runTest( + "The HTTPS_ONLY_EXEMPT flag should overrule upgrade-prefs", + "http://10.0.250.250", + "http://", + true + ); +}); + +async function runTest(desc, url, startsWith, exempt = false) { + const responseURL = await new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.timeout = 1200; + xhr.open("GET", url); + if (exempt) { + xhr.channel.loadInfo.httpsOnlyStatus |= Ci.nsILoadInfo.HTTPS_ONLY_EXEMPT; + } + xhr.onreadystatechange = () => { + // We don't care about the result and it's possible that + // the requests might even succeed in some testing environments + if ( + xhr.readyState !== XMLHttpRequest.OPENED || + xhr.readyState !== XMLHttpRequest.UNSENT + ) { + // Let's make sure this function doesn't get caled anymore + xhr.onreadystatechange = undefined; + resolve(xhr.responseURL); + } + }; + xhr.send(); + }); + ok(responseURL.startsWith(startsWith), desc); +} diff --git a/dom/security/test/https-only/browser_user_gesture.js b/dom/security/test/https-only/browser_user_gesture.js new file mode 100644 index 0000000000..0e0878e93c --- /dev/null +++ b/dom/security/test/https-only/browser_user_gesture.js @@ -0,0 +1,55 @@ +// Bug 1725026 - HTTPS Only Mode - Test if a load triggered by a user gesture +// https://bugzilla.mozilla.org/show_bug.cgi?id=1725026 +// Test if a load triggered by a user gesture can be upgraded to HTTPS +// successfully. + +"use strict"; + +const testPathUpgradeable = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +const kTestURI = testPathUpgradeable + "file_user_gesture.html"; + +add_task(async function() { + // Enable HTTPS-Only Mode and register console-listener + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + const loaded = BrowserTestUtils.browserLoaded(browser, false, null, true); + // 1. Upgrade a page to https:// + BrowserTestUtils.loadURI(browser, kTestURI); + await loaded; + await ContentTask.spawn(browser, {}, async args => { + ok( + content.document.location.href.startsWith("https://"), + "Should be https" + ); + + // 2. Trigger a load by clicking button. + // The scheme of the link url is `http` and the load should be able to + // upgraded to `https` because of HTTPS-only mode. + let button = content.document.getElementById("httpLinkButton"); + await EventUtils.synthesizeMouseAtCenter( + button, + { type: "mousedown" }, + content + ); + await EventUtils.synthesizeMouseAtCenter( + button, + { type: "mouseup" }, + content + ); + await ContentTaskUtils.waitForCondition(() => { + return content.document.location.href.startsWith("https://"); + }); + ok( + content.document.location.href.startsWith("https://"), + "Should be https" + ); + }); + }); +}); diff --git a/dom/security/test/https-only/browser_websocket_exceptions.js b/dom/security/test/https-only/browser_websocket_exceptions.js new file mode 100644 index 0000000000..3c908e2aba --- /dev/null +++ b/dom/security/test/https-only/browser_websocket_exceptions.js @@ -0,0 +1,68 @@ +"use strict"; + +const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://localhost:9898" +); + +let WEBSOCKET_DOC_URL = `${TEST_PATH_HTTP}file_websocket_exceptions.html`; + +add_task(async function() { + // Here is a sequence of how this test works: + // 1. Dynamically inject a localhost iframe + // 2. Add an exemption for localhost + // 3. Fire up Websocket + // Generally local IP addresses are exempt from https-only, but if we do not add + // an exemption for localhost, then the TriggeringPrincipal of the WebSocket is + // `not` exempt and we would upgrade ws to wss. + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_only_mode", true], + ["network.proxy.allow_hijacking_localhost", true], + ], + }); + + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + let loaded = BrowserTestUtils.browserLoaded(browser); + + BrowserTestUtils.loadURI(browser, WEBSOCKET_DOC_URL); + await loaded; + + await SpecialPowers.spawn(browser, [], async function() { + // Part 1: + let myIframe = content.document.createElement("iframe"); + content.document.body.appendChild(myIframe); + myIframe.src = + "http://localhost:9898/browser/dom/security/test/https-only/file_websocket_exceptions_iframe.html"; + + myIframe.onload = async function() { + // Part 2: + await SpecialPowers.pushPermissions([ + { + type: "https-only-load-insecure", + allow: true, + context: "http://localhost:9898", + }, + ]); + // Part 3. + myIframe.contentWindow.postMessage({ myMessage: "runWebSocket" }, "*"); + }; + + const promise = new Promise(resolve => { + content.addEventListener("WebSocketEnded", resolve, { + once: true, + }); + }); + + const { detail } = await promise; + + is(detail.state, "onopen", "sanity: websocket loaded"); + ok( + detail.url.startsWith("ws://example.com/tests"), + "exempt websocket should not be upgraded to wss://" + ); + }); + }); + await SpecialPowers.popPermissions(); +}); diff --git a/dom/security/test/https-only/file_background_redirect.sjs b/dom/security/test/https-only/file_background_redirect.sjs new file mode 100644 index 0000000000..45e01797e3 --- /dev/null +++ b/dom/security/test/https-only/file_background_redirect.sjs @@ -0,0 +1,32 @@ +// Custom *.sjs file specifically for the needs of Bug 1683015 +"use strict"; + +let { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +async function handleRequest(request, response) { + // avoid confusing cache behaviour + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + let query = request.queryString; + if (request.scheme === "https" && query === "start") { + // Simulating long repsonse time by processing the https request + // using a 5 seconds delay. Note that the http background request + // gets send after 3 seconds + response.processAsync(); + setTimeout(() => { + response.setStatusLine("1.1", 200, "OK"); + response.write( + "<html><body>Test Page for Bug 1683015 loaded</body></html>" + ); + response.finish(); + }, 5000); /* wait 5 seconds */ + return; + } + + // we should never get here, but just in case return something unexpected + response.setStatusLine("1.1", 404, "Not Found"); + response.write("<html><body>SHOULDN'T DISPLAY THIS PAGE</body></html>"); +} diff --git a/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs b/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs new file mode 100644 index 0000000000..a8a9083ef3 --- /dev/null +++ b/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs @@ -0,0 +1,67 @@ +// Custom *.sjs file specifically for the needs of Bug 1691888 +"use strict"; + +const REDIRECT_META = ` + <html> + <head> + <meta http-equiv="refresh" content="0; url='http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs?test1b'"> + </head> + <body> + META REDIRECT + </body> + </html>`; + +const REDIRECT_JS = ` + <html> + <body> + JS REDIRECT + <script> + let url= "http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs?test2b"; + window.location = url; + </script> + </body> + </html>`; + +const REDIRECT_302 = + "http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs?test3b"; + +const REDIRECT_302_DIFFERENT_PATH = + "http://example.com/tests/dom/security/test/https-only/file_user_gesture.html"; + +function handleRequest(request, response) { + // avoid confusing cache behaviour + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + // if the scheme is not https, meaning that the initial request did not + // get upgraded, then we rather fall through and display unexpected content. + if (request.scheme === "https") { + let query = request.queryString; + + if (query === "test1a") { + response.write(REDIRECT_META); + return; + } + + if (query === "test2a") { + response.write(REDIRECT_JS); + return; + } + + if (query === "test3a") { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", REDIRECT_302, false); + return; + } + + if (query === "test4a") { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", REDIRECT_302_DIFFERENT_PATH, false); + return; + } + } + + // we should never get here, just in case, + // let's return something unexpected + response.write("<html><body>DO NOT DISPLAY THIS</body></html>"); +} diff --git a/dom/security/test/https-only/file_console_logging.html b/dom/security/test/https-only/file_console_logging.html new file mode 100644 index 0000000000..94e07cddd9 --- /dev/null +++ b/dom/security/test/https-only/file_console_logging.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1625448 - HTTPS Only Mode - Tests for console logging</title> +</head> +<body> + <!-- These files don't exist since we only care if the webserver can respond. --> + <!-- This request can get upgraded. --> + <img src="http://example.com/file_1.jpg"> + <!-- This request can't get upgraded --> + <img src="http://self-signed.example.com/file_2.jpg"> + + <iframe src="http://self-signed.example.com/browser/dom/security/test/https-only/file_console_logging.html"></iframe> +</body> +</html> diff --git a/dom/security/test/https-only/file_cors_mixedcontent.html b/dom/security/test/https-only/file_cors_mixedcontent.html new file mode 100644 index 0000000000..50d32954ef --- /dev/null +++ b/dom/security/test/https-only/file_cors_mixedcontent.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + + <script> + addEventListener("StartFetch", async function() { + let comResult; + let orgResult; + + await Promise.all([ + fetch("http://example.com/") + .then(() => { + comResult = "success"; + }) + .catch(() => { + comResult = "error"; + }), + fetch("http://example.org/") + .then(() => { + orgResult = "success"; + }) + .catch(() => { + orgResult = "error"; + }), + ]); + + window.dispatchEvent(new CustomEvent("FetchEnded", { + detail: { comResult, orgResult } + })); + + }) + </script> +</head> + +<body> + <h2>Https-Only: CORS and MixedContent tests</h2> + <p><a href="https://bugzilla.mozilla.org/bug/1659505">Bug 1659505</a></p> +</body> + +</html> diff --git a/dom/security/test/https-only/file_fragment.html b/dom/security/test/https-only/file_fragment.html new file mode 100644 index 0000000000..10a9973a37 --- /dev/null +++ b/dom/security/test/https-only/file_fragment.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<script> +// sends url of current window and onbeforeunloadMessage +// when we enter here test failed. +function beforeunload(){ + window.opener.postMessage({ + info: "before-unload", + result: window.location.hash, + button: false, + }, "*"); +} + +// sends url of current window and then click on button +window.onload = function (){ + // get button by ID + let button = window.document.getElementById("clickMeButton"); + // if getting button was successful, buttonExist = true + let buttonExist = button !== null; + // send loading message + window.opener.postMessage({ + info: "onload", + result: window.location.href, + button: buttonExist, + }, "*"); + // click button + button.click(); + +} +// after button clicked and paged scrolled sends URL of current window +window.onscroll = function(){ + window.opener.postMessage({ + info: "scrolled-to-foo", + result: window.location.hash, + button: true, + }, "*"); + } + + +</script> +<body onbeforeunload="/*just to notify if we load a new page*/ beforeunload()";> + <a id="clickMeButton" href="http://example.com/tests/dom/security/test/https-only/file_fragment.html#foo">Click me</a> + <div style="height: 1000px; border: 1px solid black;"> space</div> + <a name="foo" href="http://example.com/tests/dom/security/test/https-only/file_fragment.html">foo</a> + <div style="height: 1000px; border: 1px solid black;">space</div> +</body> +</html> diff --git a/dom/security/test/https-only/file_http_background_auth_request.sjs b/dom/security/test/https-only/file_http_background_auth_request.sjs new file mode 100644 index 0000000000..80fc8ffcf7 --- /dev/null +++ b/dom/security/test/https-only/file_http_background_auth_request.sjs @@ -0,0 +1,16 @@ +// Custom *.sjs file specifically for the needs of Bug 1665062 + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if (request.scheme === "https") { + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="bug1665062"'); + return; + } + + // we should never get here; just in case, return something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/https-only/file_http_background_request.sjs b/dom/security/test/https-only/file_http_background_request.sjs new file mode 100644 index 0000000000..ef0e2ce5bd --- /dev/null +++ b/dom/security/test/https-only/file_http_background_request.sjs @@ -0,0 +1,15 @@ +// Custom *.sjs file specifically for the needs of Bug 1663396 + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if (request.scheme === "https") { + // Simulating a timeout by processing the https request + // async and *never* return anything! + response.processAsync(); + return; + } + // we should never get here; just in case, return something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/https-only/file_httpsonly_speculative_connect.html b/dom/security/test/https-only/file_httpsonly_speculative_connect.html new file mode 100644 index 0000000000..46a10401f9 --- /dev/null +++ b/dom/security/test/https-only/file_httpsonly_speculative_connect.html @@ -0,0 +1 @@ +<html><body>dummy file for speculative https-only upgrade test</body></html> diff --git a/dom/security/test/https-only/file_iframe_test.sjs b/dom/security/test/https-only/file_iframe_test.sjs new file mode 100644 index 0000000000..611870c87c --- /dev/null +++ b/dom/security/test/https-only/file_iframe_test.sjs @@ -0,0 +1,58 @@ +// Bug 1658264 - HTTPS-Only and iFrames +// see browser_iframe_test.js + +const IFRAME_CONTENT = ` +<!DOCTYPE HTML> +<html> + <head><meta charset="utf-8"></head> + <body>Helo Friend!</body> +</html>`; +const DOCUMENT_CONTENT = q => ` +<!DOCTYPE HTML> +<html> + <head><meta charset="utf-8"></head> + <body> + <iframe src="http://example.com/browser/dom/security/test/https-only/file_iframe_test.sjs?com-${q}"></iframe> + <iframe src="http://example.org/browser/dom/security/test/https-only/file_iframe_test.sjs?org-${q}"></iframe> + </body> +</html>`; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + let queryString = request.queryString; + const queryScheme = request.scheme; + + // Setup the state with an empty string and return "ok" + if (queryString == "setup") { + setState("receivedQueries", ""); + response.write("ok"); + return; + } + + let receivedQueries = getState("receivedQueries"); + + // Return result-string + if (queryString == "results") { + response.write(receivedQueries); + return; + } + + // Add semicolon to seperate strings + if (receivedQueries !== "") { + receivedQueries += ";"; + } + + // Requests from iFrames start with com or org + if (queryString.startsWith("com-") || queryString.startsWith("org-")) { + receivedQueries += queryString; + setState("receivedQueries", `${receivedQueries}-${queryScheme}`); + response.write(IFRAME_CONTENT); + return; + } + + // Everything else has to be a top-level request + receivedQueries += `top-${queryString}`; + setState("receivedQueries", `${receivedQueries}-${queryScheme}`); + response.write(DOCUMENT_CONTENT(queryString)); +} diff --git a/dom/security/test/https-only/file_insecure_reload.sjs b/dom/security/test/https-only/file_insecure_reload.sjs new file mode 100644 index 0000000000..a48ec4e20f --- /dev/null +++ b/dom/security/test/https-only/file_insecure_reload.sjs @@ -0,0 +1,27 @@ +// https://bugzilla.mozilla.org/show_bug.cgi?id=1702001 + +// An onload postmessage to window opener +const ON_LOAD = ` + <html> + <body> + send onload message... + <script type="application/javascript"> + window.opener.postMessage({result: 'you entered the http page', historyLength: history.length}, '*'); + </script> + </body> + </html>`; + +// When an https request is sent, cause a timeout so that the https-only error +// page is displayed. +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + if (request.scheme === "https") { + // Simulating a timeout by processing the https request + // async and *never* return anything! + response.processAsync(); + return; + } + if (request.scheme === "http") { + response.write(ON_LOAD); + } +} diff --git a/dom/security/test/https-only/file_redirect.sjs b/dom/security/test/https-only/file_redirect.sjs new file mode 100644 index 0000000000..c66cbaa226 --- /dev/null +++ b/dom/security/test/https-only/file_redirect.sjs @@ -0,0 +1,37 @@ +// https://bugzilla.mozilla.org/show_bug.cgi?id=1613063 + +// Step 1. Send request with redirect queryString (eg. file_redirect.sjs?302) +// Step 2. Server responds with corresponding redirect code to http://example.com/../file_redirect.sjs?check +// Step 3. Response from ?check indicates whether the redirected request was secure or not. + +const RESPONSE_SECURE = "secure-ok"; +const RESPONSE_INSECURE = "secure-error"; +const RESPONSE_ERROR = "unexpected-query"; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + const query = request.queryString; + + // Send redirect header + if ((query >= 301 && query <= 303) || query == 307) { + const loc = + "http://example.com/tests/dom/security/test/https-only/file_redirect.sjs?check"; + response.setStatusLine(request.httpVersion, query, "Moved"); + response.setHeader("Location", loc, false); + return; + } + + // Check if scheme is http:// oder https:// + if (query == "check") { + const secure = + request.scheme == "https" ? RESPONSE_SECURE : RESPONSE_INSECURE; + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(secure); + return; + } + + // This should not happen + response.setStatusLine(request.httpVersion, 500, "OK"); + response.write(RESPONSE_ERROR); +} diff --git a/dom/security/test/https-only/file_upgrade_insecure.html b/dom/security/test/https-only/file_upgrade_insecure.html new file mode 100644 index 0000000000..d230a4fd9d --- /dev/null +++ b/dom/security/test/https-only/file_upgrade_insecure.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1613063 - HTTPS Only Mode</title> + <!-- style --> + <link rel='stylesheet' type='text/css' href='http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?style' media='screen' /> + + <!-- font --> + <style> + @font-face { + font-family: "foofont"; + src: url('http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?font'); + } + .div_foo { font-family: "foofont"; } + </style> +</head> +<body> + + <!-- images: --> + <img src="http://example.com/tests/dom/security/test/https-only/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/https-only/file_upgrade_insecure_server.sjs?redirect-image"></img> + + <!-- script: --> + <script src="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?script"></script> + + <!-- media: --> + <audio src="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?media"></audio> + + <!-- objects: --> + <object width="10" height="10" data="http://example.com/tests/dom/security/test/https-only/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/https-only/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/https-only/file_upgrade_insecure_server.sjs?xhr"); + myXHR.send(null); + </script> + + <!-- websockets: upgrade ws:// to wss://--> + <script type="application/javascript"> + // WebSocket tests are not supported on Android yet. Bug 1566168 + const { AppConstants } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + if (AppConstants.platform !== "android") { + var mySocket = new WebSocket("ws://example.com/tests/dom/security/test/https-only/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.close(); + }; + mySocket.onerror = function(e) { + // debug information for Bug 1316305 + dump(" xxx mySocket.onerror: (mySocket): " + mySocket + "\n"); + dump(" xxx mySocket.onerror: (mySocket.url): " + mySocket.url + "\n"); + dump(" xxx mySocket.onerror: (e): " + e + "\n"); + dump(" xxx mySocket.onerror: (e.message): " + e.message + "\n"); + dump(" xxx mySocket.onerror: This might be related to Bug 1316305!\n"); + 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/https-only/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/https-only/file_upgrade_insecure_server.sjs b/dom/security/test/https-only/file_upgrade_insecure_server.sjs new file mode 100644 index 0000000000..aaa1f62d80 --- /dev/null +++ b/dom/security/test/https-only/file_upgrade_insecure_server.sjs @@ -0,0 +1,112 @@ +// SJS file for HTTPS-Only Mode mochitests +// Bug 1613063 - HTTPS Only Mode + +const TOTAL_EXPECTED_REQUESTS = 11; + +const IFRAME_CONTENT = + "<!DOCTYPE HTML>" + + "<html>" + + "<head><meta charset='utf-8'>" + + "<title>Bug 1613063 - HTTPS Only Mode</title>" + + "</head>" + + "<body>" + + "<img src='http://example.com/tests/dom/security/test/https-only/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/https-only/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.includes(queryString)) { + response.write("unexpected-response"); + 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; + } + receivedQueries = getState("receivedQueries"); + queryResponse.write(receivedQueries); + queryResponse.finish(); + }); + } +} diff --git a/dom/security/test/https-only/file_upgrade_insecure_wsh.py b/dom/security/test/https-only/file_upgrade_insecure_wsh.py new file mode 100644 index 0000000000..b7159c742b --- /dev/null +++ b/dom/security/test/https-only/file_upgrade_insecure_wsh.py @@ -0,0 +1,6 @@ +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + pass diff --git a/dom/security/test/https-only/file_user_gesture.html b/dom/security/test/https-only/file_user_gesture.html new file mode 100644 index 0000000000..ac67064bf0 --- /dev/null +++ b/dom/security/test/https-only/file_user_gesture.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1725026 - HTTPS Only Mode - Test if a load triggered by a user gesture can be upgraded to HTTPS</title> +</head> +<body> + <button id="httpLinkButton" onclick="location.href='http://example.com/tests/dom/security/test/https-only/file_console_logging.html'" type="button"> + button</button> +</body> +</html> diff --git a/dom/security/test/https-only/file_websocket_exceptions.html b/dom/security/test/https-only/file_websocket_exceptions.html new file mode 100644 index 0000000000..6c6ba07480 --- /dev/null +++ b/dom/security/test/https-only/file_websocket_exceptions.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + Dummy file which gets iframe injected<br/> +</body> +</html> diff --git a/dom/security/test/https-only/file_websocket_exceptions_iframe.html b/dom/security/test/https-only/file_websocket_exceptions_iframe.html new file mode 100644 index 0000000000..23c6af2d45 --- /dev/null +++ b/dom/security/test/https-only/file_websocket_exceptions_iframe.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<script> + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + + var mySocket = new WebSocket("ws://example.com/tests/dom/security/test/https-only/file_upgrade_insecure"); + mySocket.onopen = function(e) { + parent.dispatchEvent(new CustomEvent("WebSocketEnded", { + detail: { url: mySocket.url, state: "onopen" } + })); + mySocket.close(); + }; + mySocket.onerror = function(e) { + parent.dispatchEvent(new CustomEvent("WebSocketEnded", { + detail: { url: mySocket.url, state: "onerror" } + })); + mySocket.close(); + }; +} +</script> +</head> +<body> + Https-Only: WebSocket exemption test in iframe</br> +</body> +</html> diff --git a/dom/security/test/https-only/hsts_headers.sjs b/dom/security/test/https-only/hsts_headers.sjs new file mode 100644 index 0000000000..72e82caaf3 --- /dev/null +++ b/dom/security/test/https-only/hsts_headers.sjs @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) { + if (request.queryString === "reset") { + // Reset the HSTS policy, prevent influencing other tests + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Strict-Transport-Security", "max-age=0"); + response.write("Resetting HSTS"); + return; + } + let hstsHeader = "max-age=60"; + response.setHeader("Strict-Transport-Security", hstsHeader); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + // Set header for csp upgrade + response.setHeader( + "Content-Security-Policy", + "upgrade-insecure-requests", + false + ); + response.setStatusLine(request.httpVersion, 200); + response.write("<!DOCTYPE html><html><body><h1>Ok!</h1></body></html>"); +} diff --git a/dom/security/test/https-only/mochitest.ini b/dom/security/test/https-only/mochitest.ini new file mode 100644 index 0000000000..8916e0b0e6 --- /dev/null +++ b/dom/security/test/https-only/mochitest.ini @@ -0,0 +1,31 @@ +[DEFAULT] +support-files = + file_redirect.sjs + file_upgrade_insecure.html + file_upgrade_insecure_server.sjs + file_upgrade_insecure_wsh.py +prefs = + dom.security.https_first=false + security.mixed_content.upgrade_display_content=false + +[test_resource_upgrade.html] +scheme=https +[test_redirect_upgrade.html] +scheme=https +fail-if = xorigin +[test_http_background_request.html] +support-files = file_http_background_request.sjs +[test_http_background_auth_request.html] +support-files = file_http_background_auth_request.sjs +[test_break_endless_upgrade_downgrade_loop.html] +skip-if = (toolkit == 'android') # no support for error pages, Bug 1697866 +support-files = + file_break_endless_upgrade_downgrade_loop.sjs + file_user_gesture.html +[test_user_suggestion_box.html] +skip-if = toolkit == 'android' # no https-only errorpage support in android +[test_fragment.html] +support-files = file_fragment.html +[test_insecure_reload.html] +support-files = file_insecure_reload.sjs +skip-if = toolkit == 'android' # no https-only errorpage support in android diff --git a/dom/security/test/https-only/test_break_endless_upgrade_downgrade_loop.html b/dom/security/test/https-only/test_break_endless_upgrade_downgrade_loop.html new file mode 100644 index 0000000000..847a71f378 --- /dev/null +++ b/dom/security/test/https-only/test_break_endless_upgrade_downgrade_loop.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1691888: Break endless upgrade downgrade loops when using https-only</title> +<script 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"> +"use strict"; +/* + * Description of the test: + * We perform three tests where our upgrade/downgrade redirect loop detector should break the + * endless loop: + * Test 1: Meta Refresh + * Test 2: JS Redirect + * Test 3: 302 redirect + */ + +SimpleTest.requestFlakyTimeout("We need to wait for the HTTPS-Only error page to appear"); +SimpleTest.requestLongerTimeout(10); +SimpleTest.waitForExplicitFinish(); + +const REQUEST_URL = + "http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs"; + +function resolveAfter6Seconds() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 6000); + }); +} + +async function verifyResult(aTestName) { + let errorPageL10nId = "about-httpsonly-title-alert"; + let innerHTML = content.document.body.innerHTML; + ok(innerHTML.includes(errorPageL10nId), "the error page should be shown for " + aTestName); +} + +async function verifyTest4Result() { + let pathname = content.document.location.pathname; + ok( + pathname === "/tests/dom/security/test/https-only/file_user_gesture.html", + "the http:// page should be loaded" + ); +} + +async function runTests() { + await SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_only_mode", true], + ["dom.security.https_only_mode_break_upgrade_downgrade_endless_loop", true], + ["dom.security.https_only_check_path_upgrade_downgrade_endless_loop", true], + ]}); + + // Test 1: Meta Refresh Redirect + let winTest1 = window.open(REQUEST_URL + "?test1a", "_blank"); + // Test 2: JS win.location Redirect + let winTest2 = window.open(REQUEST_URL + "?test2a", "_blank"); + // Test 3: 302 Redirect + let winTest3 = window.open(REQUEST_URL + "?test3a", "_blank"); + // Test 4: 302 Redirect with a different path + let winTest4 = window.open(REQUEST_URL + "?test4a", "_blank"); + + // provide enough time for: + // the redirects to occur, and the error page to be displayed + await resolveAfter6Seconds(); + + await SpecialPowers.spawn(winTest1, ["test1"], verifyResult); + winTest1.close(); + + await SpecialPowers.spawn(winTest2, ["test2"], verifyResult); + winTest2.close(); + + await SpecialPowers.spawn(winTest3, ["test3"], verifyResult); + winTest3.close(); + + await SpecialPowers.spawn(winTest4, ["test4"], verifyTest4Result); + winTest4.close(); + + SimpleTest.finish(); +} + +runTests(); + +</script> +</body> +</html> diff --git a/dom/security/test/https-only/test_fragment.html b/dom/security/test/https-only/test_fragment.html new file mode 100644 index 0000000000..445905c550 --- /dev/null +++ b/dom/security/test/https-only/test_fragment.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1694932: Https-only mode reloads the page in certain cases when there should be just a fragment navigation </title> +<script 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"> +"use strict"; +/* + * Description of the test: + * Perform a test where a button click leads to scroll the page. Test if https-only detects + * that the redirection address of the button is on the same page. Instead of a reload https-only + * should only scroll. + * + * Test: + * Enable https-only and load the url + * Load: http://mozilla.pettay.fi/moztests/fragment.html + * Click "Click me" + * The page should be scrolled down to 'foo' without a reload + * It shouldn't receive a message 'before unload' because the on before unload + * function in file_fragment.html should not be called + */ + +SimpleTest.waitForExplicitFinish(); + +const REQUEST_URL = "http://example.com/tests/dom/security/test/https-only/file_fragment.html"; +let winTest = null; +let checkButtonClicked = false; + +async function receiveMessage(event) { + let data = event.data; + // checks if click was successful + if (!checkButtonClicked){ + // expected window location ( expected URL) + ok(data.result == "https://example.com/tests/dom/security/test/https-only/file_fragment.html", "location is correct"); + ok(data.button, "button is clicked"); + // checks if loading was successful + ok(data.info == "onload", "Onloading worked"); + // button was clicked + checkButtonClicked = true; + return; + } + // if Button was clicked once -> test finished + // check if hash exist and if hash of location is correct + ok(data.button, "button is clicked"); + ok(data.result == "#foo", "location (hash) is correct"); + // check that page is scrolled not reloaded + ok(data.info == "scrolled-to-foo","Scrolled successfully without reloading!"); + // complete test and close window + window.removeEventListener("message",receiveMessage); + winTest.close(); + SimpleTest.finish(); +} + +async function runTest() { + //Test: With https-only mode activated + await SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_only_mode", true], + ]}); + winTest = window.open(REQUEST_URL); +} +window.addEventListener("message", receiveMessage); +runTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/https-only/test_http_background_auth_request.html b/dom/security/test/https-only/test_http_background_auth_request.html new file mode 100644 index 0000000000..7fc800c388 --- /dev/null +++ b/dom/security/test/https-only/test_http_background_auth_request.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1665062 - HTTPS-Only: Do not cancel channel if auth is in progress</title> + <script 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"> + +/* + * Description of the test: + * We send a top-level request which results in a '401 - Unauthorized' and ensure that the + * http background request does not accidentally treat that request as a potential timeout. + * We make sure that ther HTTPS-Only Mode Error Page does *NOT* show up. + */ + +const { AppConstants } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("When Auth is in progress, HTTPS-Only page should not show up"); +SimpleTest.requestLongerTimeout(10); + +const EXPECTED_KICK_OFF_REQUEST = + "http://test1.example.com/tests/dom/security/test/https-only/file_http_background_auth_request.sjs?foo"; +const EXPECTED_UPGRADE_REQUEST = EXPECTED_KICK_OFF_REQUEST.replace("http://", "https://"); +let EXPECTED_BG_REQUEST = "http://test1.example.com/"; +let requestCounter = 0; + +function examiner() { + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(subject, topic, data) { + if (topic !== "specialpowers-http-notify-request") { + return; + } + + // On Android we have other requests appear here as well. Let's make + // sure we only evaluate requests triggered by the test. + if (!data.startsWith("http://test1.example.com") && + !data.startsWith("https://test1.example.com")) { + return; + } + ++requestCounter; + if (requestCounter == 1) { + is(data, EXPECTED_KICK_OFF_REQUEST, "kick off request needs to be http"); + return; + } + if (requestCounter == 2) { + is(data, EXPECTED_UPGRADE_REQUEST, "upgraded request needs to be https"); + return; + } + if (requestCounter == 3) { + is(data, EXPECTED_BG_REQUEST, "background request needs to be http and no sensitive info"); + return; + } + ok(false, "we should never get here, but just in case"); + }, + remove() { + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} +window.AuthBackgroundRequestExaminer = new examiner(); + +// https-only top-level background request occurs after 3 seconds, hence +// we use 4 seconds to make sure the background request did not happen. +function resolveAfter4Seconds() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 4000); + }); +} + +async function runTests() { + await SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_only_mode", true], + ["dom.security.https_only_mode_send_http_background_request", true], + ]}); + + let testWin = window.open(EXPECTED_KICK_OFF_REQUEST, "_blank"); + + // Give the Auth Process and background request some time before moving on. + await resolveAfter4Seconds(); + + if (AppConstants.platform !== "android") { + is(requestCounter, 3, "three requests total (kickoff, upgraded, background)"); + } else { + // On Android, the auth request resolves and hence the background request + // is not even kicked off - nevertheless, the error page should not appear! + is(requestCounter, 2, "two requests total (kickoff, upgraded)"); + } + + await SpecialPowers.spawn(testWin, [], () => { + let innerHTML = content.document.body.innerHTML; + is(innerHTML, "", "expection page should not be displayed"); + }); + + testWin.close(); + + window.AuthBackgroundRequestExaminer.remove(); + SimpleTest.finish(); +} + +runTests(); + +</script> +</body> +</html> diff --git a/dom/security/test/https-only/test_http_background_request.html b/dom/security/test/https-only/test_http_background_request.html new file mode 100644 index 0000000000..5dff0a5fdb --- /dev/null +++ b/dom/security/test/https-only/test_http_background_request.html @@ -0,0 +1,125 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1663396: Test HTTPS-Only-Mode top-level background request not leaking sensitive info</title> + <script 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"> + +/* + * Description of the test: + * Send a top-level request and make sure that the the top-level https-only background request + * (a) does only use pre-path information + * (b) does not happen if the pref is set to false + */ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("have to test that https-only mode background request does not happen"); +SimpleTest.requestLongerTimeout(8); + +const SJS_PATH = "tests/dom/security/test/https-only/file_http_background_request.sjs?sensitive"; + +const EXPECTED_KICK_OFF_REQUEST = "http://example.com/" + SJS_PATH; +const EXPECTED_KICK_OFF_REQUEST_LOCAL = "http://localhost:8/" + SJS_PATH; +const EXPECTED_UPGRADE_REQUEST = EXPECTED_KICK_OFF_REQUEST.replace("http://", "https://"); +let expectedBackgroundRequest = ""; +let requestCounter = 0; + +function examiner() { + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(subject, topic, data) { + if (topic !== "specialpowers-http-notify-request") { + return; + } + // On Android we have other requests appear here as well. Let's make + // sure we only evaluate requests triggered by the test. + if (!data.startsWith("http://example.com") && + !data.startsWith("https://example.com") && + !data.startsWith("http://localhost:8") && + !data.startsWith("https://localhost:8")) { + return; + } + ++requestCounter; + if (requestCounter == 1) { + ok( + data === EXPECTED_KICK_OFF_REQUEST || data === EXPECTED_KICK_OFF_REQUEST_LOCAL, + "kick off request needs to be http" + ); + return; + } + if (requestCounter == 2) { + is(data, EXPECTED_UPGRADE_REQUEST, "upgraded request needs to be https"); + return; + } + if (requestCounter == 3) { + is(data, expectedBackgroundRequest, "background request needs to be http and no sensitive info like path"); + return; + } + ok(false, "we should never get here, but just in case"); + }, + remove() { + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} +window.BackgroundRequestExaminer = new examiner(); + +// https-only top-level background request occurs after 3 seconds, hence +// we use 4 seconds to make sure the background request did not happen. +function resolveAfter4Seconds() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 4000); + }); +} + +async function runTests() { + // (a) Test http background request to only use prePath information + expectedBackgroundRequest = "http://example.com/"; + requestCounter = 0; + await SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_only_mode", true], + ["dom.security.https_only_mode_send_http_background_request", true], + ["dom.security.https_only_mode_error_page_user_suggestions", false], + ]}); + let testWin = window.open(EXPECTED_KICK_OFF_REQUEST, "_blank"); + await resolveAfter4Seconds(); + is(requestCounter, 3, "three requests total (kickoff, upgraded, background)"); + testWin.close(); + + // (x) Test no http background request happens when localhost + expectedBackgroundRequest = ""; + requestCounter = 0; + testWin = window.open(EXPECTED_KICK_OFF_REQUEST_LOCAL, "_blank"); + await resolveAfter4Seconds(); + is(requestCounter, 1, "one requests total (kickoff, no upgraded, no background)"); + testWin.close(); + + // (b) Test no http background request happens if pref is set to false + expectedBackgroundRequest = ""; + requestCounter = 0; + await SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_only_mode", true], + ["dom.security.https_only_mode_send_http_background_request", false], + ["dom.security.https_only_mode_error_page_user_suggestions", false], + ]}); + testWin = window.open(EXPECTED_KICK_OFF_REQUEST, "_blank"); + await resolveAfter4Seconds(); + is(requestCounter, 2, "two requests total (kickoff, upgraded, no background)"); + testWin.close(); + + // clean up and finish tests + window.BackgroundRequestExaminer.remove(); + SimpleTest.finish(); +} + +runTests(); + +</script> +</body> +</html> diff --git a/dom/security/test/https-only/test_insecure_reload.html b/dom/security/test/https-only/test_insecure_reload.html new file mode 100644 index 0000000000..d143c9080b --- /dev/null +++ b/dom/security/test/https-only/test_insecure_reload.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1702001: Https-only mode does not reload pages after clicking "Continue to HTTP Site", when url contains navigation </title> +<script 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"> +"use strict"; +/* + * Description of the test: + * + * Load a page including a fragment portion which does not support https and make + * sure that exempting the page from https-only-mode does not result in a fragment + * navigation. + */ + + + function resolveAfter6Seconds() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 6000); + }); +} + +SimpleTest.requestFlakyTimeout("We need to wait for the HTTPS-Only error page to appear"); +SimpleTest.waitForExplicitFinish(); + +let winTest = null; +let TEST_URL = "http://example.com/tests/dom/security/test/https-only/file_insecure_reload.sjs#nav"; + +// verify that https-only page appeared +async function verifyErrorPage() { + let errorPageL10nId = "about-httpsonly-title-alert"; + let innerHTML = content.document.body.innerHTML; + ok(innerHTML.includes(errorPageL10nId), "the error page should be shown for "); + let button = content.document.getElementById("openInsecure"); + // Click "Continue to HTTP Site" + ok(button, "button exist"); + if(button) { + button.click(); + } +} +// verify that you entered the page and are not still displaying +// the https-only error page +async function receiveMessage(event) { + // read event + let { result, historyLength } = event.data; + is(result, "you entered the http page", "The requested page should be shown"); + is(historyLength, 1, "History should contain one item"); + window.removeEventListener("message",receiveMessage); + winTest.close(); + SimpleTest.finish(); +} + + +async function runTest() { + //Test: With https-only mode activated + await SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_only_mode", true], + ]}); + winTest = window.open(TEST_URL); + await resolveAfter6Seconds(); + await SpecialPowers.spawn(winTest,[],verifyErrorPage); +} +window.addEventListener("message", receiveMessage); +runTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/https-only/test_redirect_upgrade.html b/dom/security/test/https-only/test_redirect_upgrade.html new file mode 100644 index 0000000000..59f02f96d0 --- /dev/null +++ b/dom/security/test/https-only/test_redirect_upgrade.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1613063 +Test that 302 redirect requests get upgraded to https:// with HTTPS-Only Mode enabled +--> + +<head> + <title>HTTPS-Only Mode - XHR Redirect Upgrade</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + <h1>HTTPS-Only Mode</h1> + <p>Upgrade Test for insecure XHR redirects.</p> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1613063">Bug 1613063</a> + + <script type="application/javascript"> + + const redirectCodes = ["301", "302", "303", "307"] + let currentTest = 0 + + function startTest() { + const currentCode = redirectCodes[currentTest]; + + const myXHR = new XMLHttpRequest(); + // Make a request to a site (eg. https://file_redirect.sjs?301), which will redirect to http://file_redirect.sjs?check. + // The response will either be secure-ok, if the request has been upgraded to https:// or secure-error if it didn't. + myXHR.open("GET", `https://example.com/tests/dom/security/test/https-only/file_redirect.sjs?${currentCode}`); + myXHR.onload = (e) => { + is(myXHR.responseText, "secure-ok", `a ${currentCode} redirect when posting violation report should be blocked`) + testDone(); + } + // This should not happen + myXHR.onerror = (e) => { + ok(false, `Could not query results from server for ${currentCode}-redirect test (" + e.message + ")`); + testDone(); + } + myXHR.send(); + } + + function testDone() { + // Check if there are remaining tests + if (++currentTest < redirectCodes.length) { + startTest() + } else { + SimpleTest.finish(); + } + } + + SimpleTest.waitForExplicitFinish(); + // Set preference and start test + SpecialPowers.pushPrefEnv({ set: [["dom.security.https_only_mode", true]] }, startTest); + + </script> +</body> +</html> diff --git a/dom/security/test/https-only/test_resource_upgrade.html b/dom/security/test/https-only/test_resource_upgrade.html new file mode 100644 index 0000000000..a5fb9b6cad --- /dev/null +++ b/dom/security/test/https-only/test_resource_upgrade.html @@ -0,0 +1,122 @@ +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>HTTPS-Only Mode - Resource Upgrade</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + <h1>HTTPS-Only Mode</h1> + <p>Upgrade Test for various resources</p> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1613063">Bug 1613063</a> + <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 + * preference "dom.security.https_only_mode" is set to true. 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 { AppConstants } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + const splitRegex = /^(.*)-(.*)$/ + const testConfig = { + topLevelScheme: "http://", + results: [ + "iframe", "script", "img", "img-redir", "font", "xhr", "style", + "media", "object", "form", "nested-img" + ] + } + // TODO: WebSocket tests are not supported on Android Yet. Bug 1566168. + if (AppConstants.platform !== "android") { + testConfig.results.push("websocket"); + } + + + function runTest() { + // 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) { + checkResult(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(() => { + var src = testConfig.topLevelScheme + "example.com/tests/dom/security/test/https-only/file_upgrade_insecure.html"; + document.getElementById("testframe").src = src; + }); + } + + // 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); + function receiveMessage(event) { + checkResult(event.data.result); + } + + function finishTest() { + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); + } + + function checkResult(response) { + // A response looks either like this "iframe-ok" or "[key]-[result]" + const [, key, result] = splitRegex.exec(response) + // try to find the expected result within the results array + var index = testConfig.results.indexOf(key); + + // If the response is not even part of the results array, something is super wrong + if (index == -1) { + ok(false, `Unexpected response from server (${response})`); + finishTest(); + } + + // take the element out the array and continue till the results array is empty + if (index != -1) { + testConfig.results.splice(index, 1); + } + + // Check if the result was okay or had an error + is(result, 'ok', `Upgrade all requests on toplevel http for '${key}' came back with: '${result}'`) + + // If we're not expecting any more resulsts, finish the test + if (!testConfig.results.length) { + finishTest(); + } + } + + SimpleTest.waitForExplicitFinish(); + + // Set preference and start test + SpecialPowers.pushPrefEnv({ set: [["dom.security.https_only_mode", true]] }, runTest); + + </script> +</body> + +</html> diff --git a/dom/security/test/https-only/test_user_suggestion_box.html b/dom/security/test/https-only/test_user_suggestion_box.html new file mode 100644 index 0000000000..1feabcd003 --- /dev/null +++ b/dom/security/test/https-only/test_user_suggestion_box.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<html> +<head> + <title>Bug 1665057 - Add www button on https-only error page</title> + <script 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"> +"use strict"; + +/* + * Description of the test: + * We send a top-level request to a http-page in https-only mode + * The page has a bad certificate and can't be updated so the error page appears + * If there is a secure connection possible to www the suggestion-box on the error page + * should appear and have a link to that secure www-page + * if the original-pagerequest already has a www or there is no secure www connection + * the suggestion box should not appear + */ + +SimpleTest.requestFlakyTimeout("We need to wait for the HTTPS-Only error page to appear and for the additional 'www' request to provide a suggestion."); +SimpleTest.requestLongerTimeout(10); + +// urls of server locations with bad cert -> https-only should display error page +const KICK_OFF_REQUEST_WITH_SUGGESTION = "http://suggestion-example.com"; +const KICK_OFF_REQUEST_WITHOUT_SUGGESTION = "http://no-suggestion-example.com"; + +// l10n ids to compare html to +const ERROR_PAGE_L10N_ID = "about-httpsonly-title-alert"; +const SUGGESTION_BOX_L10N_ID = "about-httpsonly-suggestion-box-www-text"; + +// the error page needs to be build and a background https://www request needs to happen +// we use 4 seconds to make sure these requests did happen. +function resolveAfter4Seconds() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 4000); + }); +} + +async function runTests(aMessage) { + let errorPageL10nId = "about-httpsonly-title-alert"; + let suggestionBoxL10nId = "about-httpsonly-suggestion-box-www-text"; + + let innerHTML = content.document.body.innerHTML; + + if (aMessage === "with_suggestion") { + // test if the page with suggestion shows the error page and the suggestion box + ok(innerHTML.includes(errorPageL10nId), "the error page should be shown."); + ok(innerHTML.includes(suggestionBoxL10nId), "the suggestion box should be shown."); + } else if (aMessage === "without_suggestion") { + // test if the page without suggestion shows the error page but not the suggestion box + ok(innerHTML.includes(errorPageL10nId), "the error page should be shown."); + ok(!innerHTML.includes(suggestionBoxL10nId), "the suggestion box should not be shown."); + } else { + ok(false, "we should never get here"); + } +} + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_only_mode", true], + ["dom.security.https_only_mode_send_http_background_request", false], + ["dom.security.https_only_mode_error_page_user_suggestions", true], + ]}); + let testWinSuggestion = window.open(KICK_OFF_REQUEST_WITH_SUGGESTION, "_blank"); + let testWinWithoutSuggestion = window.open(KICK_OFF_REQUEST_WITHOUT_SUGGESTION, "_blank"); + + await resolveAfter4Seconds(); + + await SpecialPowers.spawn(testWinSuggestion, ["with_suggestion"], runTests); + await SpecialPowers.spawn(testWinWithoutSuggestion, ["without_suggestion"], runTests); + + testWinSuggestion.close(); + testWinWithoutSuggestion.close(); +}); +</script> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/auto_upgrading_identity.html b/dom/security/test/mixedcontentblocker/auto_upgrading_identity.html new file mode 100644 index 0000000000..d843b7fae1 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/auto_upgrading_identity.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset='utf-8'> + <title>Bug 1674341: Test SiteIdentity when auto-upgrading mixed content</title> +</head> +<body> + <!-- needs to be http: image for mixed content and auto-upgrading --> + <img type="image/png" id="testimage" src="http://example.com/browser/dom/security/test/mixedcontentblocker/auto_upgrading_identity.png" /> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/auto_upgrading_identity.png b/dom/security/test/mixedcontentblocker/auto_upgrading_identity.png Binary files differnew file mode 100644 index 0000000000..52c591798e --- /dev/null +++ b/dom/security/test/mixedcontentblocker/auto_upgrading_identity.png diff --git a/dom/security/test/mixedcontentblocker/browser.ini b/dom/security/test/mixedcontentblocker/browser.ini new file mode 100644 index 0000000000..e68e0680e9 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/browser.ini @@ -0,0 +1,19 @@ +[DEFAULT] +prefs = + dom.security.https_first=false +support-files = + download_page.html + download_server.sjs + +[browser_test_mixed_content_download.js] +skip-if = win10_2004 && debug # Bug 1723573 +[browser_mixed_content_auto_upgrade_display_console.js] +support-files = file_mixed_content_auto_upgrade_display_console.html +[browser_auto_upgrading_identity.js] +support-files = + auto_upgrading_identity.html + auto_upgrading_identity.png +[browser_mixed_content_auth_download.js] +support-files = + file_auth_download_page.html + file_auth_download_server.sjs diff --git a/dom/security/test/mixedcontentblocker/browser_auto_upgrading_identity.js b/dom/security/test/mixedcontentblocker/browser_auto_upgrading_identity.js new file mode 100644 index 0000000000..3ae924a4dc --- /dev/null +++ b/dom/security/test/mixedcontentblocker/browser_auto_upgrading_identity.js @@ -0,0 +1,49 @@ +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const TEST_TOPLEVEL_URI = TEST_PATH + "auto_upgrading_identity.html"; + +// auto upgrading mixed content should not indicate passive mixed content loaded +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [["security.mixed_content.upgrade_display_content", true]], + }); + await BrowserTestUtils.withNewTab(TEST_TOPLEVEL_URI, async function(browser) { + await ContentTask.spawn(browser, {}, async function() { + let testImg = content.document.getElementById("testimage"); + ok( + testImg.src.includes("auto_upgrading_identity.png"), + "sanity: correct image is loaded" + ); + }); + // Ensure the identiy handler does not show mixed content! + ok( + !gIdentityHandler._isMixedPassiveContentLoaded, + "Auto-Upgrading Mixed Content: Identity should note indicate mixed content" + ); + }); +}); + +// regular mixed content test should indicate passive mixed content loaded +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [["security.mixed_content.upgrade_display_content", false]], + }); + await BrowserTestUtils.withNewTab(TEST_TOPLEVEL_URI, async function(browser) { + await ContentTask.spawn(browser, {}, async function() { + let testImg = content.document.getElementById("testimage"); + ok( + testImg.src.includes("auto_upgrading_identity.png"), + "sanity: correct image is loaded" + ); + }); + // Ensure the identiy handler does show mixed content! + ok( + gIdentityHandler._isMixedPassiveContentLoaded, + "Regular Mixed Content: Identity should indicate mixed content" + ); + }); +}); diff --git a/dom/security/test/mixedcontentblocker/browser_mixed_content_auth_download.js b/dom/security/test/mixedcontentblocker/browser_mixed_content_auth_download.js new file mode 100644 index 0000000000..492317e6ad --- /dev/null +++ b/dom/security/test/mixedcontentblocker/browser_mixed_content_auth_download.js @@ -0,0 +1,168 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + Downloads: "resource://gre/modules/Downloads.sys.mjs", + DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs", +}); + +const { PromptTestUtils } = ChromeUtils.import( + "resource://testing-common/PromptTestUtils.jsm" +); + +let authPromptModalType = Services.prefs.getIntPref( + "prompts.modalType.httpAuth" +); + +const downloadMonitoringView = { + _listeners: [], + onDownloadAdded(download) { + for (let listener of this._listeners) { + listener(download); + } + this._listeners = []; + }, + waitForDownload(listener) { + this._listeners.push(listener); + }, +}; + +let SECURE_BASE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "https://example.com/" + ) + "file_auth_download_page.html"; + +/** + * Waits until a download is triggered. + * It waits until a prompt is shown, + * saves and then accepts the dialog. + * @returns {Promise} Resolved once done. + */ + +function shouldTriggerDownload() { + return new Promise(res => { + downloadMonitoringView.waitForDownload(res); + }); +} +function shouldNotifyDownloadUI() { + return new Promise(res => { + downloadMonitoringView.waitForDownload(async aDownload => { + let { error } = aDownload; + if ( + error.becauseBlockedByReputationCheck && + error.reputationCheckVerdict == Downloads.Error.BLOCK_VERDICT_INSECURE + ) { + // It's an insecure Download, now Check that it has been cleaned up properly + if ((await IOUtils.stat(aDownload.target.path)).size != 0) { + throw new Error(`Download target is not empty!`); + } + if ((await IOUtils.stat(aDownload.target.path)).size != 0) { + throw new Error(`Download partFile was not cleaned up properly`); + } + // Assert that the Referrer is present + if (!aDownload.source.referrerInfo) { + throw new Error("The Blocked download is missing the ReferrerInfo"); + } + + res(aDownload); + } else { + ok(false, "No error for download that was expected to error!"); + } + }); + }); +} + +async function resetDownloads() { + // Removes all downloads from the download List + const types = new Set(); + let publicList = await Downloads.getList(Downloads.ALL); + let downloads = await publicList.getAll(); + for (let download of downloads) { + if (download.contentType) { + types.add(download.contentType); + } + publicList.remove(download); + await download.finalize(true); + } +} + +async function runTest(url, link, checkFunction, description) { + await resetDownloads(); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + is( + gBrowser.currentURI.schemeIs("https"), + true, + "Scheme of opened tab should be https" + ); + info("Checking: " + description); + + let checkPromise = checkFunction(); + // Click the Link to trigger the download + SpecialPowers.spawn(gBrowser.selectedBrowser, [link], contentLink => { + content.document.getElementById(contentLink).click(); + }); + // Wait for the auth prompt, enter the login details and close the prompt + await PromptTestUtils.handleNextPrompt( + gBrowser.selectedBrowser, + { modalType: authPromptModalType, promptType: "promptUserAndPass" }, + { buttonNumClick: 0, loginInput: "user", passwordInput: "pass" } + ); + await checkPromise; + ok(true, description); + // Close download panel + DownloadsPanel.hidePanel(); + is(DownloadsPanel.panel.state, "closed", "Panel should be closed"); + await BrowserTestUtils.removeTab(tab); +} + +add_setup(async function() { + let list = await Downloads.getList(Downloads.ALL); + list.addView(downloadMonitoringView); + registerCleanupFunction(() => list.removeView(downloadMonitoringView)); + // Ensure to delete all cached credentials before running test + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve); + }); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.download.improvements_to_download_panel", true], + ["dom.block_download_insecure", true], + ], + }); +}); +//Test description: +// 1. Open "https://example.com". +// 2. From "https://example.com" download something, but that download is only available via http +// and with authentication. +// 3. Login and start download. +// 4. Mixed-content blocker blocks download. +// 5. Unblock download and verify the downloaded file. +add_task(async function test_auth_download() { + await runTest( + SECURE_BASE_URL, + "insecure", + async () => { + let [, download] = await Promise.all([ + shouldTriggerDownload(), + shouldNotifyDownloadUI(), + ]); + await download.unblock(); + ok(download.error == null, "There should be no error after unblocking"); + info( + "Start download to be able to validate the size and the success of the download" + ); + await download.start(); + is( + download.contentType, + "text/html", + "File contentType should be correct." + ); + ok(download.succeeded, "Download succeeded!"); + is(download.target.size, 27, "Download has correct size"); + }, + "A locked Download from an auth server should succeeded to Download after a Manual unblock" + ); +}); diff --git a/dom/security/test/mixedcontentblocker/browser_mixed_content_auto_upgrade_display_console.js b/dom/security/test/mixedcontentblocker/browser_mixed_content_auto_upgrade_display_console.js new file mode 100644 index 0000000000..1da3e770a3 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/browser_mixed_content_auto_upgrade_display_console.js @@ -0,0 +1,51 @@ +// Bug 1673574 - Improve Console logging for mixed content auto upgrading +"use strict"; + +const testPath = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +let seenAutoUpgradeMessage = false; + +const kTestURI = + testPath + "file_mixed_content_auto_upgrade_display_console.html"; + +add_task(async function() { + // A longer timeout is necessary for this test than the plain mochitests + // due to opening a new tab with the web console. + requestLongerTimeout(4); + + // Enable HTTPS-Only Mode and register console-listener + await SpecialPowers.pushPrefEnv({ + set: [["security.mixed_content.upgrade_display_content", true]], + }); + Services.console.registerListener(on_auto_upgrade_message); + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, kTestURI); + + await BrowserTestUtils.waitForCondition(() => seenAutoUpgradeMessage); + + Services.console.unregisterListener(on_auto_upgrade_message); +}); + +function on_auto_upgrade_message(msgObj) { + const message = msgObj.message; + + // The console message is: + // "Mixed Content: Upgrading insecure display request + // ‘http://example.com/file_mixed_content_auto_upgrade_display_console.jpg’ to use ‘https’" + + if (!message.includes("Mixed Content:")) { + return; + } + ok( + message.includes("Upgrading insecure display request"), + "msg includes info" + ); + ok( + message.includes("file_mixed_content_auto_upgrade_display_console.jpg"), + "msg includes file" + ); + seenAutoUpgradeMessage = true; +} diff --git a/dom/security/test/mixedcontentblocker/browser_test_mixed_content_download.js b/dom/security/test/mixedcontentblocker/browser_test_mixed_content_download.js new file mode 100644 index 0000000000..18405adee5 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/browser_test_mixed_content_download.js @@ -0,0 +1,345 @@ +ChromeUtils.defineESModuleGetters(this, { + Downloads: "resource://gre/modules/Downloads.sys.mjs", + DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs", +}); + +const HandlerService = Cc[ + "@mozilla.org/uriloader/handler-service;1" +].getService(Ci.nsIHandlerService); + +const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + +let INSECURE_BASE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://example.com/" + ) + "download_page.html"; +let SECURE_BASE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "https://example.com/" + ) + "download_page.html"; + +function promiseFocus() { + return new Promise(resolve => { + waitForFocus(resolve); + }); +} + +function promisePanelOpened() { + if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") { + return Promise.resolve(); + } + return BrowserTestUtils.waitForEvent(DownloadsPanel.panel, "popupshown"); +} + +async function task_openPanel() { + await promiseFocus(); + + let promise = promisePanelOpened(); + DownloadsPanel.showPanel(); + await promise; +} + +const downloadMonitoringView = { + _listeners: [], + onDownloadAdded(download) { + for (let listener of this._listeners) { + listener(download); + } + this._listeners = []; + }, + waitForDownload(listener) { + this._listeners.push(listener); + }, +}; + +/** + * Waits until a download is triggered. + * Unless the always_ask_before_handling_new_types pref is true, the download + * will simply be saved, so resolve when the view is notified of the new + * download. Otherwise, it waits until a prompt is shown, selects the choosen + * <action>, then accepts the dialog + * @param [action] Which action to select, either: + * "handleInternally", "save" or "open". + * @returns {Promise} Resolved once done. + */ + +function shouldTriggerDownload(action = "save") { + if ( + Services.prefs.getBoolPref( + "browser.download.always_ask_before_handling_new_types" + ) + ) { + return new Promise((resolve, reject) => { + Services.wm.addListener({ + onOpenWindow(xulWin) { + Services.wm.removeListener(this); + let win = xulWin.docShell.domWindow; + waitForFocus(() => { + if ( + win.location == + "chrome://mozapps/content/downloads/unknownContentType.xhtml" + ) { + let dialog = win.document.getElementById("unknownContentType"); + let button = dialog.getButton("accept"); + let actionRadio = win.document.getElementById(action); + actionRadio.click(); + button.disabled = false; + dialog.acceptDialog(); + resolve(); + } else { + reject(); + } + }, win); + }, + }); + }); + } + return new Promise(res => { + downloadMonitoringView.waitForDownload(res); + }); +} + +const CONSOLE_ERROR_MESSAGE = "Blocked downloading insecure content"; + +function shouldConsoleError() { + // Waits until CONSOLE_ERROR_MESSAGE was logged + return new Promise((resolve, reject) => { + function listener(msgObj) { + let text = msgObj.message; + if (text.includes(CONSOLE_ERROR_MESSAGE)) { + Services.console.unregisterListener(listener); + resolve(); + } + } + Services.console.registerListener(listener); + }); +} + +async function resetDownloads() { + // Removes all downloads from the download List + const types = new Set(); + let publicList = await Downloads.getList(Downloads.PUBLIC); + let downloads = await publicList.getAll(); + for (let download of downloads) { + if (download.contentType) { + types.add(download.contentType); + } + publicList.remove(download); + await download.finalize(true); + } + + if (types.size) { + // reset handlers for the contentTypes of any files previously downloaded + for (let type of types) { + const mimeInfo = MIMEService.getFromTypeAndExtension(type, ""); + info("resetting handler for type: " + type); + HandlerService.remove(mimeInfo); + } + } +} + +function shouldNotifyDownloadUI() { + return new Promise(res => { + downloadMonitoringView.waitForDownload(async aDownload => { + let { error } = aDownload; + if ( + error.becauseBlockedByReputationCheck && + error.reputationCheckVerdict == Downloads.Error.BLOCK_VERDICT_INSECURE + ) { + // It's an insecure Download, now Check that it has been cleaned up properly + if ((await IOUtils.stat(aDownload.target.path)).size != 0) { + throw new Error(`Download target is not empty!`); + } + if ((await IOUtils.stat(aDownload.target.path)).size != 0) { + throw new Error(`Download partFile was not cleaned up properly`); + } + // Assert that the Referrer is presnt + if (!aDownload.source.referrerInfo) { + throw new Error("The Blocked download is missing the ReferrerInfo"); + } + + res(aDownload); + } else { + ok(false, "No error for download that was expected to error!"); + } + }); + }); +} + +async function runTest(url, link, checkFunction, description) { + await SpecialPowers.pushPrefEnv({ + set: [["dom.block_download_insecure", true]], + }); + await resetDownloads(); + + let tab = BrowserTestUtils.addTab(gBrowser, url); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Checking: " + description); + + let checkPromise = checkFunction(); + // Click the Link to trigger the download + SpecialPowers.spawn(gBrowser.selectedBrowser, [link], contentLink => { + content.document.getElementById(contentLink).click(); + }); + + await checkPromise; + + ok(true, description); + BrowserTestUtils.removeTab(tab); + + await SpecialPowers.popPrefEnv(); +} + +add_setup(async () => { + let list = await Downloads.getList(Downloads.ALL); + list.addView(downloadMonitoringView); + registerCleanupFunction(() => list.removeView(downloadMonitoringView)); +}); + +// Test Blocking +add_task(async function test_blocking() { + for (let prefVal of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.download.always_ask_before_handling_new_types", prefVal], + ["browser.download.improvements_to_download_panel", !prefVal], + ], + }); + await runTest( + INSECURE_BASE_URL, + "insecure", + shouldTriggerDownload, + "Insecure -> Insecure should download" + ); + await runTest( + INSECURE_BASE_URL, + "secure", + shouldTriggerDownload, + "Insecure -> Secure should download" + ); + await runTest( + SECURE_BASE_URL, + "insecure", + () => + Promise.all([ + shouldTriggerDownload(), + shouldNotifyDownloadUI(), + shouldConsoleError(), + ]), + "Secure -> Insecure should Error" + ); + await runTest( + SECURE_BASE_URL, + "secure", + shouldTriggerDownload, + "Secure -> Secure should Download" + ); + await SpecialPowers.popPrefEnv(); + } +}); + +// Test Manual Unblocking +add_task(async function test_manual_unblocking() { + for (let prefVal of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.download.always_ask_before_handling_new_types", prefVal], + ["browser.download.improvements_to_download_panel", !prefVal], + ], + }); + await runTest( + SECURE_BASE_URL, + "insecure", + async () => { + let [, download] = await Promise.all([ + shouldTriggerDownload(), + shouldNotifyDownloadUI(), + ]); + await download.unblock(); + ok(download.error == null, "There should be no error after unblocking"); + }, + "A Blocked Download Should succeeded to Download after a Manual unblock" + ); + await SpecialPowers.popPrefEnv(); + } +}); + +// Test Unblock Download Visible +add_task(async function test_unblock_download_visible() { + for (let prefVal of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.download.always_ask_before_handling_new_types", prefVal], + ["browser.download.improvements_to_download_panel", !prefVal], + ], + }); + // Focus, open and close the panel once + // to make sure the panel is loaded and ready + await promiseFocus(); + await runTest( + SECURE_BASE_URL, + "insecure", + async () => { + let panelHasOpened = promisePanelOpened(); + info("awaiting that the download is triggered and added to the list"); + await Promise.all([shouldTriggerDownload(), shouldNotifyDownloadUI()]); + info("awaiting that the Download list shows itself"); + await panelHasOpened; + DownloadsPanel.hidePanel(); + ok(true, "The Download Panel should have opened on blocked download"); + }, + "A Blocked Download Should open the Download Panel" + ); + await SpecialPowers.popPrefEnv(); + } +}); + +// Test Download an insecure svg and choose "Open with Firefox" +add_task(async function download_open_insecure_SVG() { + const mimeInfo = MIMEService.getFromTypeAndExtension("image/svg+xml", "svg"); + mimeInfo.alwaysAskBeforeHandling = false; + mimeInfo.preferredAction = mimeInfo.handleInternally; + HandlerService.store(mimeInfo); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.download.always_ask_before_handling_new_types", false], + ["browser.download.improvements_to_download_panel", true], + ], + }); + await promiseFocus(); + await runTest( + SECURE_BASE_URL, + "insecureSVG", + async () => { + info("awaiting that the download is triggered and added to the list"); + let [_, download] = await Promise.all([ + shouldTriggerDownload("handleInternally"), + shouldNotifyDownloadUI(), + ]); + + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser); + await download.unblock(); + ok(download.error == null, "There should be no error after unblocking"); + + let tab = await newTabPromise; + + ok( + tab.linkedBrowser._documentURI.filePath.includes(".svg"), + "The download target was opened" + ); + BrowserTestUtils.removeTab(tab); + ok(true, "The Content was opened in a new tab"); + await SpecialPowers.popPrefEnv(); + }, + "A Blocked SVG can be opened internally" + ); + + HandlerService.remove(mimeInfo); +}); diff --git a/dom/security/test/mixedcontentblocker/download_page.html b/dom/security/test/mixedcontentblocker/download_page.html new file mode 100644 index 0000000000..86f605c478 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/download_page.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=676619 +--> + <head> + + <title>Test for the download attribute</title> + + </head> + <body> + hi + + <script> + const host = window.location.host; + const path = location.pathname.replace("download_page.html","download_server.sjs"); + + const secureLink = document.createElement("a"); + secureLink.href=`https://${host}/${path}`; + secureLink.download="true"; + secureLink.textContent="Secure Link"; + secureLink.id="secure"; + + const insecureLink = document.createElement("a"); + insecureLink.href=`http://${host}/${path}`; + insecureLink.download="true"; + insecureLink.id="insecure"; + insecureLink.textContent="Not secure Link"; + + const insecureSVG = document.createElement("a"); + insecureSVG.href=`http://${host}/${path}?type=image/svg+xml&name=example.svg`; + insecureSVG.download="true"; + insecureSVG.id="insecureSVG"; + insecureSVG.textContent="Not secure Link to SVG"; + + document.body.append(insecureSVG); + document.body.append(secureLink); + document.body.append(insecureLink); + </script> + </body> +</html> diff --git a/dom/security/test/mixedcontentblocker/download_server.sjs b/dom/security/test/mixedcontentblocker/download_server.sjs new file mode 100644 index 0000000000..e659df2f40 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/download_server.sjs @@ -0,0 +1,20 @@ +// force the Browser to Show a Download Prompt + +function handleRequest(request, response) { + let type = "image/png"; + let filename = "hello.png"; + request.queryString.split("&").forEach(val => { + var [key, value] = val.split("="); + if (key == "type") { + type = value; + } + if (key == "name") { + filename = value; + } + }); + + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Disposition", `attachment; filename=${filename}`); + response.setHeader("Content-Type", type); + response.write("🙈🙊ðŸµðŸ™Š"); +} diff --git a/dom/security/test/mixedcontentblocker/file_auth_download_page.html b/dom/security/test/mixedcontentblocker/file_auth_download_page.html new file mode 100644 index 0000000000..931d9d00ad --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_auth_download_page.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test mixed content download with auth header by https-first</title> + </head> + <body> + hi + + <script> + const host = window.location.host; + const path = location.pathname.replace("file_auth_download_page.html","file_auth_download_server.sjs"); + + const insecureLink = document.createElement("a"); + insecureLink.href = `http://${host}${path}?user=user&pass=pass`; + insecureLink.download = "true"; + insecureLink.id = "insecure"; + insecureLink.textContent = "Not secure Link"; + + document.body.append(insecureLink); + </script> + </body> +</html> diff --git a/dom/security/test/mixedcontentblocker/file_auth_download_server.sjs b/dom/security/test/mixedcontentblocker/file_auth_download_server.sjs new file mode 100644 index 0000000000..d28625abb6 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_auth_download_server.sjs @@ -0,0 +1,62 @@ +"use strict"; + +function handleRequest(request, response) { + let match; + + // Allow the caller to drive how authentication is processed via the query. + // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar + // The extra ? allows the user/pass/realm checks to succeed if the name is + // at the beginning of the query string. + Components.utils.importGlobalProperties(["URLSearchParams"]); + let query = new URLSearchParams(request.queryString); + + let expected_user = query.get("user"); + let expected_pass = query.get("pass"); + let realm = query.get("realm"); + + // Look for an authentication header, if any, in the request. + // + // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== + // + // This test only supports Basic auth. The value sent by the client is + // "username:password", obscured with base64 encoding. + + let actual_user = "", + actual_pass = "", + authHeader; + if (request.hasHeader("Authorization")) { + authHeader = request.getHeader("Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) { + throw new Error("Couldn't parse auth header: " + authHeader); + } + // Decode base64 to string + let userpass = atob(match[1]); + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) { + throw new Error("Couldn't decode auth header: " + userpass); + } + actual_user = match[1]; + actual_pass = match[2]; + } + + // Don't request authentication if the credentials we got were what we + // expected. + let requestAuth = + expected_user != actual_user || expected_pass != actual_pass; + + if (requestAuth) { + response.setStatusLine("1.0", 401, "Authentication required"); + response.setHeader("WWW-Authenticate", 'basic realm="' + realm + '"', true); + response.write("Authentication required"); + } else { + response.setStatusLine("1.0", 200, "OK"); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader( + "Content-Disposition", + "attachment; filename=dummy-file.html" + ); + response.setHeader("Content-Type", "text/html"); + response.write("<p id='success'>SUCCESS</p>"); + } +} diff --git a/dom/security/test/mixedcontentblocker/file_bug1551886.html b/dom/security/test/mixedcontentblocker/file_bug1551886.html new file mode 100644 index 0000000000..41c46b5273 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_bug1551886.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> +</head> + +<body> +<script> + let f = document.createElement("iframe"); + f.src = "data:text/html,<iframe src='http://example.com' onload=\"parent.postMessage({status:'loaded', type: 'http'}, 'https://example.com')\" onerror=\"parent.postMessage({status:'blocked', type: 'http'}, 'https://example.com')\"></iframe>"; + window.addEventListener("message", (event) => { + parent.postMessage(event.data, "http://mochi.test:8888"); + + // Only create second iframe once + if(event.data.type === "https") { + return; + } + + let f2 = document.createElement("iframe"); + f2.src = "data:text/html,<iframe src='https://example.com' onload=\"parent.postMessage({status:'loaded', type: 'https'}, 'https://example.com')\" onerror=\"parent.postMessage({status:'blocked', type: 'https'}, 'https://example.com')\"></iframe>"; + document.body.appendChild(f2); + }); + document.body.appendChild(f); +</script> +</body> +</html> 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 0000000000..f1459d3667 --- /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 0000000000..fd9ea23178 --- /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 0000000000..c4502ce8d2 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_frameNavigation_blankTarget.html @@ -0,0 +1,33 @@ +<!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" rel="opener">Go to http site</a> + +<script> + var blankTarget = document.getElementById("blankTarget"); + blankTarget.click(); + + var observer = { + observe(subject, topic, data) { + if (topic === "specialpowers-http-notify-request" && + data === "http://example.com/tests/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html?blankTarget") { + parent.parent.postMessage({"test": "blankTarget", "msg": "opened an http link with target=_blank from a secure page"}, "http://mochi.test:8888"); + SpecialPowers.removeObserver(observer, "specialpowers-http-notify-request"); + } + } + } + + // 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 + SpecialPowers.addObserver(observer, "specialpowers-http-notify-request"); +</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 0000000000..10cf1e4d90 --- /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.includes("Permission denied to access property")) { + // 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 0000000000..251bb73e33 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_frameNavigation_innermost.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<body> +<div id="content"></div> +<script> + // get the case from the query string + var type = location.search.substring(1); + + function clickLink() { + // 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); + }); + } + + 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>'; + clickLink(); + 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>'; + clickLink(); + 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>'; + clickLink(); + 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>'; + clickLink(); + break; + + case "securePage_navigate_grandchild_response": + parent.parent.parent.postMessage({"test": "securePage_navigate_grandchild", "msg": "navigated to insecure grandchild iframe on secure page"}, "http://mochi.test:8888"); + 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 0000000000..692d542067 --- /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": "navigating to insecure iframe blocked on secure page"}, "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 0000000000..235ac0055e --- /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.includes("Permission denied to access property")) { + // 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 0000000000..e4a2d8d08b --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_main.html @@ -0,0 +1,338 @@ +<!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 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> + function ok(a, msg) { + parent.postMessage({msg, check: true, status: !!a}, "http://mochi.test:8888"); + } + + function is(a, b, msg) { + ok(a == b, msg); + } + + function report(type, msg) { + parent.postMessage({test: type, msg}, "http://mochi.test:8888"); + } + + function uniqueID() { + return Math.floor(Math.random() * 100000) + } + function uniqueIDParam(id) { + return "&uniqueID=" + id; + } + + async function init() { + var baseUrl = "http://example.com/tests/dom/security/test/mixedcontentblocker/file_server.sjs"; + var checkLastRequestUrl = "https://example.com/tests/dom/security/test/mixedcontentblocker/file_server.sjs?lastRequest=true"; + + //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"); + + async function checkLastRequest() { + const response = await fetch(checkLastRequestUrl); + return response.json(); + } + + /* Part 1: Mixed Script tests */ + + // Test 1a: insecure object + var object = document.createElement("object"); + var objectId = uniqueID(); + object.data = baseUrl + "?type=object" + uniqueIDParam(objectId); + object.type = "image/png"; + object.width = "200"; + object.height = "200"; + + testContent.appendChild(object); + + var objectCount = 0; + + function objectStatus(object) { + // Expose our privileged bits on the object. We will match the MIME type to the one + // provided by file_server.sjs + object = SpecialPowers.wrap(object); + var typeIsSet = object.actualType && (object.actualType !== ''); + var isLoaded = typeIsSet && object.actualType === 'application/x-test-match'; + if (isLoaded) { + //object loaded + report("object", "insecure object loaded"); + } + else { + if(!typeIsSet && objectCount < MAX_COUNT) { + objectCount++; + setTimeout(objectStatus, TIMEOUT_INTERVAL, object); + } + else { + //After we have called setTimeout the maximum number of times, assume object is blocked + report("object", "insecure object blocked"); + } + } + } + + // 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 scriptId = uniqueID(); + var scriptLoad = false; + var scriptCount = 0; + script.src = baseUrl + "?type=script" + uniqueIDParam(scriptId); + script.onload = function(e) { + report("script", "insecure script loaded"); + 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 + report("script", "insecure script blocked"); + } + } + } + + // 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"); + var cssStyleSheetId = uniqueID(); + cssStyleSheet.rel = "stylesheet"; + cssStyleSheet.href = baseUrl + "?type=stylesheet" + uniqueIDParam(cssStyleSheetId); + cssStyleSheet.type = "text/css"; + testContent.appendChild(cssStyleSheet); + + var styleCount = 0; + + function styleStatus(cssStyleSheet) { + if( cssStyleSheet.sheet || cssStyleSheet.styleSheet || cssStyleSheet.innerHTML ) { + report("stylesheet", "insecure stylesheet loaded"); + } + 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 + report("stylesheet", "insecure stylesheet blocked"); + } + } + } + + // 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"); + var iframeId = uniqueID(); + iframe.src = baseUrl + "?type=iframe" + uniqueIDParam(iframeId); + iframe.onload = function() { + report("iframe", "insecure iframe loaded"); + } + iframe.onerror = function() { + report("iframe", "insecure iframe blocked"); + }; + testContent.appendChild(iframe); + + + // Test 1e: insecure xhr + await new Promise((resolve) => { + var xhr = new XMLHttpRequest; + var xhrId = uniqueID(); + try { + xhr.open("GET", baseUrl + "?type=xhr" + uniqueIDParam(xhrId), true); + xhr.send(); + xhr.onloadend = function (oEvent) { + if (xhr.status == 200) { + report("xhr", "insecure xhr loaded"); + resolve(); + } + else { + report("xhr", "insecure xhr blocked"); + resolve(); + } + } + } catch(ex) { + report("xhr", "insecure xhr blocked"); + resolve(); + } + }); + + /* Part 2: Mixed Display tests */ + + // Shorthand for all image test variants + async function imgHandlers(img, test, uniqueID) { + await new Promise((resolve) => { + img.onload = async () => { + const lastRequest = await checkLastRequest(); + is(lastRequest.uniqueID, uniqueID, "UniqueID for the last request matches"); + let message = "insecure image loaded"; + if (lastRequest.scheme == "https") { + message = "secure image loaded after upgrade"; + } + report(test, message); + resolve(); + } + img.onerror = async () => { + let message = "insecure image blocked"; + report(test, message); + resolve(); + } + }); + } + + // Test 2a: insecure image + var img = document.createElement("img"); + var imgId = uniqueID(); + img.src = baseUrl + "?type=img" + uniqueIDParam(imgId); + await imgHandlers(img, "image", imgId); + // 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"); + var mediaId = uniqueID(); + media.src = baseUrl + "?type=media" + uniqueIDParam(mediaId); + media.width = "320"; + media.height = "200"; + media.type = "video/ogg"; + await new Promise(res => { + media.onloadeddata = async () => { + const lastRequest = await checkLastRequest(); + is(lastRequest.uniqueID, mediaId, "UniqueID for the last request matches"); + let message = "insecure media loaded"; + if (lastRequest.scheme == "https") { + message = "secure media loaded after upgrade"; + } + report("media", message); + res(); + } + media.onerror = function() { + report("media", "insecure media blocked"); + res(); + } + }); + // 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"); + var imgAId = uniqueID(); + imgA.srcset = baseUrl + "?type=img&subtype=imageSrcset" + uniqueIDParam(imgAId); + await imgHandlers(imgA, "imageSrcset", imgAId); + + // Test 3b: image with srcset, using fallback from src, should still use imageset policy + var imgB = document.createElement("img"); + var imgBId = uniqueID(); + imgB.srcset = " "; + imgB.src = baseUrl + "?type=img&subtype=imageSrcSetFallback" + uniqueIDParam(imgBId); + await imgHandlers(imgB, "imageSrcsetFallback", imgBId); + + // Test 3c: image in <picture> + var imgC = document.createElement("img"); + var pictureC = document.createElement("picture"); + var sourceC = document.createElement("source"); + var sourceCId = uniqueID(); + sourceC.srcset = baseUrl + "?type=img&subtype=imagePicture" + uniqueIDParam(sourceCId); + pictureC.appendChild(sourceC); + pictureC.appendChild(imgC); + await imgHandlers(imgC, "imagePicture", sourceCId); + + // 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"); + var imgDId = uniqueID(); + var sourceDId = uniqueID(); + imgD.src = baseUrl + "?type=img&subtype=imageJoinPicture" + uniqueIDParam(imgDId); + await new Promise(res => { + 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 = baseUrl + "?type=img&subtype=imageJoinPicture2" + uniqueIDParam(sourceDId); + pictureD.appendChild(sourceD); + pictureD.appendChild(imgD); + res(); + } + }); + await imgHandlers(imgD, "imageJoinPicture", sourceDId); + + // 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 pictureEId = uniqueID(); + var sourceE = document.createElement("source"); + var sourceEId = uniqueID(); + sourceE.srcset = baseUrl + "?type=img&subtype=imageLeavePicture" + uniqueIDParam(sourceEId); + pictureE.appendChild(sourceE); + pictureE.appendChild(imgE); + imgE.src = baseUrl + "?type=img&subtype=imageLeavePicture2" + uniqueIDParam(pictureEId); + await new Promise(res => { + imgE.onload = imgE.onerror = function() { + // Whether or not it loads, remove it from the picture and observe + pictureE.removeChild(imgE) + res(); + } + }); + await imgHandlers(imgE, "imageLeavePicture", pictureEId); + } + + init(); + +</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 0000000000..3404e54471 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_main_bug803225.html @@ -0,0 +1,175 @@ +<!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 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", + "wss" +--> + +<script> + const { AppConstants } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + + //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.sys.mjs"; + 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: 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 5: 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 6: mailto protocol + let mm = SpecialPowers.loadChromeScript(function launchHandler() { + /* eslint-env mozilla/chrome-script */ + 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=%"; + + 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 () { + /* eslint-env mozilla/process-script */ + var os = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + var observer = { + observe(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"); + } + + Services.ppmm.loadProcessScript(pScript, true); + + var uri = ioService.newURI("mailto:foo@bar.com"); + 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 7: wss protocol + // WebSocket tests are not supported on Android Yet. Bug 1566168. + if (AppConstants.platform !== "android") { + 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 0000000000..b7159c742b --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_main_bug803225_websocket_wsh.py @@ -0,0 +1,6 @@ +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + pass diff --git a/dom/security/test/mixedcontentblocker/file_mixed_content_auto_upgrade_display_console.html b/dom/security/test/mixedcontentblocker/file_mixed_content_auto_upgrade_display_console.html new file mode 100644 index 0000000000..b86fbc9cbc --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_mixed_content_auto_upgrade_display_console.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1673574 - Improve Console logging for mixed content auto upgrading</title> +</head> +<body> + <!-- The following file does in fact not exist because we only care if it shows up in the console --> + <img src="http://example.com/file_mixed_content_auto_upgrade_display_console.jpg"> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/file_redirect.html b/dom/security/test/mixedcontentblocker/file_redirect.html new file mode 100644 index 0000000000..99e1873791 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_redirect.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug1402363: Test mixed content redirects</title> +</head> +<body> + +<script type="text/javascript"> + const PATH = "https://example.com/tests/dom/security/test/mixedcontentblocker/"; + + // check a fetch redirect from https to https (should be allowed) + fetch(PATH + "file_redirect_handler.sjs?https-to-https-redirect", { + method: 'get' + }).then(function(response) { + window.parent.postMessage("https-to-https-loaded", "*"); + }).catch(function(err) { + window.parent.postMessage("https-to-https-blocked", "*"); + }); + + // check a fetch redirect from https to http (should be blocked) + fetch(PATH + "file_redirect_handler.sjs?https-to-http-redirect", { + method: 'get' + }).then(function(response) { + window.parent.postMessage("https-to-http-loaded", "*"); + }).catch(function(err) { + window.parent.postMessage("https-to-http-blocked", "*"); + }); + +</script> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/file_redirect_handler.sjs b/dom/security/test/mixedcontentblocker/file_redirect_handler.sjs new file mode 100644 index 0000000000..4c134c00e8 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_redirect_handler.sjs @@ -0,0 +1,35 @@ +// custom *.sjs file for +// Bug 1402363: Test Mixed Content Redirect Blocking. + +const URL_PATH = "example.com/tests/dom/security/test/mixedcontentblocker/"; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + let queryStr = request.queryString; + + if (queryStr === "https-to-https-redirect") { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader( + "Location", + "https://" + URL_PATH + "file_redirect_handler.sjs?load", + false + ); + return; + } + + if (queryStr === "https-to-http-redirect") { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader( + "Location", + "http://" + URL_PATH + "file_redirect_handler.sjs?load", + false + ); + return; + } + + if (queryStr === "load") { + response.setHeader("Content-Type", "text/html", false); + response.write("foo"); + return; + } +} diff --git a/dom/security/test/mixedcontentblocker/file_server.sjs b/dom/security/test/mixedcontentblocker/file_server.sjs new file mode 100644 index 0000000000..a13f4ab8b1 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/file_server.sjs @@ -0,0 +1,131 @@ +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +function ERR(response, msg) { + dump("ERROR: " + msg + "\n"); + response.write("HTTP/1.1 400 Bad Request\r\n"); + response.write("Content-Type: text/html; charset=UTF-8\r\n"); + response.write("Content-Length: " + msg.length + "\r\n"); + response.write("\r\n"); + response.write(msg); +} + +function loadContentFromFile(path) { + // Load the content 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 testContentFile = Components.classes[ + "@mozilla.org/file/directory_service;1" + ] + .getService(Components.interfaces.nsIProperties) + .get("CurWorkD", Components.interfaces.nsIFile); + var dirs = path.split("/"); + for (var i = 0; i < dirs.length; i++) { + testContentFile.append(dirs[i]); + } + var testContentFileStream = Components.classes[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Components.interfaces.nsIFileInputStream); + testContentFileStream.init(testContentFile, -1, 0, 0); + var testContent = NetUtil.readInputStreamToString( + testContentFileStream, + testContentFileStream.available() + ); + return testContent; +} + +function handleRequest(request, response) { + const { scheme, host, path } = request; + // get the Content-Type to serve from the query string + var contentType = null; + var uniqueID = null; + var showLastRequest = false; + request.queryString.split("&").forEach(function(val) { + var [name, value] = val.split("="); + if (name == "type") { + contentType = unescape(value); + } + if (name == "uniqueID") { + uniqueID = unescape(value); + } + if (name == "lastRequest") { + showLastRequest = true; + } + }); + + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if (showLastRequest) { + response.setHeader("Content-Type", "text/html", false); + + // We don't want to expose the same lastRequest multiple times. + var state = getState("lastRequest"); + setState("lastRequest", ""); + + if (state == "") { + ERR(response, "No last request!"); + return; + } + + response.write(state); + return; + } + + if (!uniqueID) { + ERR(response, "No uniqueID?!?"); + return; + } + + setState( + "lastRequest", + JSON.stringify({ + scheme, + host, + path, + uniqueID, + contentType: contentType || "other", + }) + ); + + switch (contentType) { + case "img": + response.setHeader("Content-Type", "image/png", false); + response.write( + loadContentFromFile("tests/image/test/mochitest/blue.png") + ); + break; + + case "media": + response.setHeader("Content-Type", "video/ogg", false); + response.write(loadContentFromFile("tests/dom/media/test/320x240.ogv")); + break; + + 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-match", 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 0000000000..2e322b6392 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/mochitest.ini @@ -0,0 +1,36 @@ +[DEFAULT] +tags = mcb +prefs = + security.mixed_content.upgrade_display_content=false + dom.security.https_first=false +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 + file_redirect.html + file_redirect_handler.sjs + file_bug1551886.html + +[test_main.html] +skip-if = + true + toolkit == 'android' + (verify && !debug && os == 'linux') # Android: TIMED_OUT; bug 1402554 + tsan # Times out / Memory consumption, bug 1612707 +[test_bug803225.html] +skip-if = (os=='linux' && bits==32) || headless || tsan # Linux32:bug 1324870; Headless:bug 1405870; tsan:bug 1612707 +[test_frameNavigation.html] +skip-if = + true # Bug 1424752 +[test_redirect.html] +[test_bug1551886.html] diff --git a/dom/security/test/mixedcontentblocker/test_bug1551886.html b/dom/security/test/mixedcontentblocker/test_bug1551886.html new file mode 100644 index 0000000000..bf128256a4 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/test_bug1551886.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1551886: Opaque documents aren't considered in the mixed content blocker</title> +<script 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"> + +SimpleTest.waitForExplicitFinish(); + +let f = document.createElement("iframe"); +f.src = "https://example.com/tests/dom/security/test/mixedcontentblocker/file_bug1551886.html"; + +window.addEventListener("message", (event) => { + switch(event.data.type) { + case 'http': + is(event.data.status, "blocked", "nested load of http://example should get blocked by the MCB"); + break + case 'https': + is(event.data.status, "loaded", "nested load of https://example should not get blocked by the MCB"); + SimpleTest.finish(); + break; + } +}); + +document.body.appendChild(f); + +</script> +</body> +</html> diff --git a/dom/security/test/mixedcontentblocker/test_bug803225.html b/dom/security/test/mixedcontentblocker/test_bug803225.html new file mode 100644 index 0000000000..56f5e02e05 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/test_bug803225.html @@ -0,0 +1,157 @@ +<!DOCTYPE HTML> +<html> +<!-- +Testing Allowlist 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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script> + const { AppConstants } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + + 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.all_resource_uri_content_accessible", true], // Temporarily allow content to access all resource:// URIs. + ["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, + resource: false, + unsafe_about: false, + data_protocol: false, + javascript: false, + }; + + if (AppConstants.platform !== "android") { + // WebSocket tests are not supported on Android Yet. Bug 1566168. + testsToRun.wss = false; + testsToRun.mailto = 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 "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); + + 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 0000000000..82e3e715d2 --- /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 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; + async 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++; + await 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); + 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 0000000000..9a13a0853f --- /dev/null +++ b/dom/security/test/mixedcontentblocker/test_main.html @@ -0,0 +1,231 @@ +<!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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script> + let counter = 0; + // blockDisplay blockActive upgradeDisplay + const settings = [ + [true, true, true], + [true, false, true], + [false, true, true], + [false, false, true], + [true, true, false], + [true, false, false], + [false, true, false], + [false, false, false], + ]; + + let blockActive; + let blockDisplay; + let upgradeDisplay; + + //Cycle through 8 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]], + ["security.mixed_content.upgrade_display_content", settings[counter][2]]]; + 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"); + upgradeDisplay = SpecialPowers.getBoolPref("security.mixed_content.upgrade_display_content"); + counter++; + callback(); + }); + } + + let 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 < 8) { + for (var prop in testsToRun) { + testsToRun[prop] = false; + } + //call to change the preferences + changePrefs([], function() { + log(`\nblockDisplay set to ${blockDisplay}, blockActive set to ${blockActive}, upgradeDisplay set to ${upgradeDisplay}`); + reloadFrame(); + }); + } + else { + SimpleTest.finish(); + } + } + + var firstTest = true; + + function receiveMessage(event) { + if(firstTest) { + log(`blockActive set to ${blockActive}, blockDisplay set to ${blockDisplay}, upgradeDisplay set to ${upgradeDisplay}.`); + firstTest = false; + } + + // Simple check from the iframe. + if (event.data.check) { + ok(event.data.status, event.data.msg); + return; + } + + 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 + if (upgradeDisplay) { + ok(event.data.msg == "secure image loaded after upgrade", "image did not follow upgrade_display_content pref"); + } else { + ok(blockDisplay == (event.data.msg == "insecure image blocked"), "image did not follow block_display_content pref"); + } + testsToRun.image = true; + break; + + case "media": + if (upgradeDisplay) { + ok(event.data.msg == "secure media loaded after upgrade", "media did not follow upgrade_display_content pref"); + } else { + 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": + // When blockDisplay && blockActive && upgradeDisplay are all true the request is blocked + // This appears to be a side effect of blockDisplay taking precedence here. + if (event.data.msg == "secure image loaded after upgrade") { + ok(upgradeDisplay, "imageSrcset did not follow upgrade_display_content pref"); + } else { + ok(blockActive == (event.data.msg == "insecure image blocked"), "imageSrcset did not follow block_active_content pref"); + } + testsToRun.imageSrcset = true; + break; + + case "imageSrcsetFallback": + if (event.data.msg == "secure image loaded after upgrade") { + ok(upgradeDisplay, "imageSrcsetFallback did not follow upgrade_display_content pref"); + } else { + ok(blockActive == (event.data.msg == "insecure image blocked"), "imageSrcsetFallback did not follow block_active_content pref"); + } + testsToRun.imageSrcsetFallback = true; + break; + + case "imagePicture": + if (event.data.msg == "secure image loaded after upgrade") { + ok(upgradeDisplay, "imagePicture did not follow upgrade_display_content pref"); + } else { + ok(blockActive == (event.data.msg == "insecure image blocked"), "imagePicture did not follow block_active_content pref"); + } + testsToRun.imagePicture = true; + break; + + case "imageJoinPicture": + if (event.data.msg == "secure image loaded after upgrade") { + ok(upgradeDisplay, "imageJoinPicture did not follow upgrade_display_content pref"); + } else { + 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": + if (event.data.msg == "secure image loaded after upgrade") { + ok(upgradeDisplay, "imageLeavePicture did not follow upgrade_display_content pref"); + } else { + ok(blockDisplay == (event.data.msg == "insecure image blocked"), "imageLeavePicture did not follow block_display_content pref"); + } + testsToRun.imageLeavePicture = true; + break; + + } + checkTestsCompleted(); + } + + function startTest() { + //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); + + //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/mixedcontentblocker/test_redirect.html b/dom/security/test/mixedcontentblocker/test_redirect.html new file mode 100644 index 0000000000..3fdd4e2e7b --- /dev/null +++ b/dom/security/test/mixedcontentblocker/test_redirect.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug1402363: Test mixed content redirects</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body onload='startTest()'> +<iframe style="width:100%;height:300px;" id="testframe"></iframe> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +const PATH = "https://example.com/tests/dom/security/test/mixedcontentblocker/"; +let testcounter = 0; + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + if (event.data === "https-to-https-loaded") { + ok(true, "https to https fetch redirect should be allowed"); + } + else if (event.data === "https-to-http-blocked") { + ok(true, "https to http fetch redirect should be blocked"); + } + else { + ok(false, "sanity: we should never enter that branch (" + event.data + ")"); + } + testcounter++; + if (testcounter < 2) { + return; + } + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); +} + +function startTest() { + let testframe = document.getElementById("testframe"); + testframe.src = PATH + "file_redirect.html"; +} + +</script> +</body> +</html> diff --git a/dom/security/test/moz.build b/dom/security/test/moz.build new file mode 100644 index 0000000000..7ea2127490 --- /dev/null +++ b/dom/security/test/moz.build @@ -0,0 +1,43 @@ +# -*- 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/. + +with Files("cors/**"): + BUG_COMPONENT = ("Core", "Networking") + +XPCSHELL_TESTS_MANIFESTS += [ + "unit/xpcshell.ini", +] + +TEST_DIRS += [ + "gtest", +] + +MOCHITEST_MANIFESTS += [ + "cors/mochitest.ini", + "csp/mochitest.ini", + "general/mochitest.ini", + "https-first/mochitest.ini", + "https-only/mochitest.ini", + "mixedcontentblocker/mochitest.ini", + "referrer-policy/mochitest.ini", + "sec-fetch/mochitest.ini", + "sri/mochitest.ini", +] + +MOCHITEST_CHROME_MANIFESTS += [ + "general/chrome.ini", +] + +BROWSER_CHROME_MANIFESTS += [ + "cors/browser.ini", + "csp/browser.ini", + "general/browser.ini", + "https-first/browser.ini", + "https-only/browser.ini", + "mixedcontentblocker/browser.ini", + "referrer-policy/browser.ini", + "sec-fetch/browser.ini", +] diff --git a/dom/security/test/referrer-policy/browser.ini b/dom/security/test/referrer-policy/browser.ini new file mode 100644 index 0000000000..4de690c85b --- /dev/null +++ b/dom/security/test/referrer-policy/browser.ini @@ -0,0 +1,5 @@ +[DEFAULT] +support-files = referrer_page.sjs + +[browser_referrer_disallow_cross_site_relaxing.js] +[browser_referrer_telemetry.js] diff --git a/dom/security/test/referrer-policy/browser_referrer_disallow_cross_site_relaxing.js b/dom/security/test/referrer-policy/browser_referrer_disallow_cross_site_relaxing.js new file mode 100644 index 0000000000..1094d9a772 --- /dev/null +++ b/dom/security/test/referrer-policy/browser_referrer_disallow_cross_site_relaxing.js @@ -0,0 +1,458 @@ +/** + * Bug 1720294 - Testing disallow relaxing default referrer policy for + * cross-site requests. + */ + +"use strict"; + +requestLongerTimeout(6); + +const TEST_DOMAIN = "https://example.com/"; +const TEST_SAME_SITE_DOMAIN = "https://test1.example.com/"; +const TEST_SAME_SITE_DOMAIN_HTTP = "http://test1.example.com/"; +const TEST_CROSS_SITE_DOMAIN = "https://test1.example.org/"; +const TEST_CROSS_SITE_DOMAIN_HTTP = "http://test1.example.org/"; + +const TEST_PATH = "browser/dom/security/test/referrer-policy/"; + +const TEST_PAGE = `${TEST_DOMAIN}${TEST_PATH}referrer_page.sjs`; +const TEST_SAME_SITE_PAGE = `${TEST_SAME_SITE_DOMAIN}${TEST_PATH}referrer_page.sjs`; +const TEST_SAME_SITE_PAGE_HTTP = `${TEST_SAME_SITE_DOMAIN_HTTP}${TEST_PATH}referrer_page.sjs`; +const TEST_CROSS_SITE_PAGE = `${TEST_CROSS_SITE_DOMAIN}${TEST_PATH}referrer_page.sjs`; +const TEST_CROSS_SITE_PAGE_HTTP = `${TEST_CROSS_SITE_DOMAIN_HTTP}${TEST_PATH}referrer_page.sjs`; + +const REFERRER_FULL = 0; +const REFERRER_ORIGIN = 1; +const REFERRER_NONE = 2; + +function getExpectedReferrer(referrer, type) { + let res; + + switch (type) { + case REFERRER_FULL: + res = referrer; + break; + case REFERRER_ORIGIN: + let url = new URL(referrer); + res = `${url.origin}/`; + break; + case REFERRER_NONE: + res = ""; + break; + default: + ok(false, "unknown type"); + } + + return res; +} + +async function verifyResultInPage(browser, expected) { + await SpecialPowers.spawn(browser, [expected], value => { + is(content.document.referrer, value, "The document.referrer is correct."); + + let result = content.document.getElementById("result"); + is(result.textContent, value, "The referer header is correct"); + }); +} + +function getExpectedConsoleMessage(expected, isPrefOn, url) { + let msg; + + if (isPrefOn) { + msg = + "Referrer Policy: Ignoring the less restricted referrer policy “" + + expected + + "†for the cross-site request: " + + url; + } else { + msg = + "Referrer Policy: Less restricted policies, including " + + "‘no-referrer-when-downgrade’, ‘origin-when-cross-origin’ and " + + "‘unsafe-url’, will be ignored soon for the cross-site request: " + + url; + } + + return msg; +} + +function createConsoleMessageVerificationPromise(expected, isPrefOn, url) { + if (!expected) { + return Promise.resolve(); + } + + return new Promise(resolve => { + let listener = { + observe(msg) { + let message = msg.QueryInterface(Ci.nsIScriptError); + + if (message.category.startsWith("Security")) { + is( + message.errorMessage, + getExpectedConsoleMessage(expected, isPrefOn, url), + "The console message is correct." + ); + Services.console.unregisterListener(listener); + resolve(); + } + }, + }; + + Services.console.registerListener(listener); + }); +} + +function verifyNoConsoleMessage() { + // Verify that there is no referrer policy console message. + let allMessages = Services.console.getMessageArray(); + + for (let msg of allMessages) { + let message = msg.QueryInterface(Ci.nsIScriptError); + if ( + message.category.startsWith("Security") && + message.errorMessage.startsWith("Referrer Policy:") + ) { + ok(false, "There should be no console message for referrer policy."); + } + } +} + +const TEST_CASES = [ + // Testing that the referrer policy can be overridden with less restricted + // policy in the same-origin scenario. + { + policy: "unsafe-url", + referrer: TEST_PAGE, + test_url: TEST_PAGE, + expect: REFERRER_FULL, + original: REFERRER_FULL, + }, + // Testing that the referrer policy can be overridden with less restricted + // policy in the same-site scenario. + { + policy: "unsafe-url", + referrer: TEST_PAGE, + test_url: TEST_SAME_SITE_PAGE, + expect: REFERRER_FULL, + original: REFERRER_FULL, + }, + { + policy: "no-referrer-when-downgrade", + referrer: TEST_PAGE, + test_url: TEST_SAME_SITE_PAGE, + expect: REFERRER_FULL, + original: REFERRER_FULL, + }, + { + policy: "origin-when-cross-origin", + referrer: TEST_PAGE, + test_url: TEST_SAME_SITE_PAGE_HTTP, + expect: REFERRER_ORIGIN, + original: REFERRER_ORIGIN, + }, + // Testing that the referrer policy cannot be overridden with less restricted + // policy in the cross-site scenario. + { + policy: "unsafe-url", + referrer: TEST_PAGE, + test_url: TEST_CROSS_SITE_PAGE, + expect: REFERRER_ORIGIN, + expect_console: "unsafe-url", + original: REFERRER_FULL, + }, + { + policy: "no-referrer-when-downgrade", + referrer: TEST_PAGE, + test_url: TEST_CROSS_SITE_PAGE, + expect: REFERRER_ORIGIN, + expect_console: "no-referrer-when-downgrade", + original: REFERRER_FULL, + }, + { + policy: "origin-when-cross-origin", + referrer: TEST_PAGE, + test_url: TEST_CROSS_SITE_PAGE_HTTP, + expect: REFERRER_NONE, + expect_console: "origin-when-cross-origin", + original: REFERRER_ORIGIN, + }, + // Testing that the referrer policy can still be overridden with more + // restricted policy in the cross-site scenario. + { + policy: "no-referrer", + referrer: TEST_PAGE, + test_url: TEST_CROSS_SITE_PAGE, + expect: REFERRER_NONE, + original: REFERRER_NONE, + }, +]; + +add_setup(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable mixed content blocking to be able to test downgrade scenario. + ["security.mixed_content.block_active_content", false], + ], + }); +}); + +async function runTestIniFrame(gBrowser, enabled, expectNoConsole) { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async browser => { + for (let type of ["meta", "header"]) { + for (let test of TEST_CASES) { + info(`Test iframe: ${test.toSource()}`); + let referrerURL = `${test.referrer}?${type}=${test.policy}`; + let expected = enabled + ? getExpectedReferrer(referrerURL, test.expect) + : getExpectedReferrer(referrerURL, test.original); + + let expected_console = expectNoConsole + ? undefined + : test.expect_console; + + Services.console.reset(); + + BrowserTestUtils.loadURI(browser, referrerURL); + await BrowserTestUtils.browserLoaded(browser, false, referrerURL); + + let iframeURL = test.test_url + "?show"; + + let consolePromise = createConsoleMessageVerificationPromise( + expected_console, + enabled, + iframeURL + ); + // Create an iframe and load the url. + let bc = await SpecialPowers.spawn( + browser, + [iframeURL], + async url => { + let iframe = content.document.createElement("iframe"); + let loadPromise = ContentTaskUtils.waitForEvent(iframe, "load"); + iframe.src = url; + content.document.body.appendChild(iframe); + + await loadPromise; + + return iframe.browsingContext; + } + ); + + await verifyResultInPage(bc, expected); + await consolePromise; + if (!expected_console) { + verifyNoConsoleMessage(); + } + } + } + } + ); +} + +async function runTestForLinkClick(gBrowser, enabled, expectNoConsole) { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async browser => { + for (let type of ["meta", "header"]) { + for (let test of TEST_CASES) { + info(`Test link click: ${test.toSource()}`); + let referrerURL = `${test.referrer}?${type}=${test.policy}`; + let expected = enabled + ? getExpectedReferrer(referrerURL, test.expect) + : getExpectedReferrer(referrerURL, test.original); + + let expected_console = expectNoConsole + ? undefined + : test.expect_console; + + Services.console.reset(); + + BrowserTestUtils.loadURI(browser, referrerURL); + await BrowserTestUtils.browserLoaded(browser, false, referrerURL); + + let linkURL = test.test_url + "?show"; + + let consolePromise = createConsoleMessageVerificationPromise( + expected_console, + enabled, + linkURL + ); + + // Create the promise to wait for the navigation finishes. + let loadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + linkURL + ); + + // Generate the link and click it to navigate. + await SpecialPowers.spawn(browser, [linkURL], async url => { + let link = content.document.createElement("a"); + link.textContent = "Link"; + link.setAttribute("href", url); + + content.document.body.appendChild(link); + link.click(); + }); + + await loadedPromise; + + await verifyResultInPage(browser, expected); + await consolePromise; + if (!expected_console) { + verifyNoConsoleMessage(); + } + } + } + } + ); +} + +async function toggleETPForPage(gBrowser, url, toggle) { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + // First, Toggle ETP off for the test page. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + url + ); + + if (toggle) { + gProtectionsHandler.enableForCurrentPage(); + } else { + gProtectionsHandler.disableForCurrentPage(); + } + + await browserLoadedPromise; + BrowserTestUtils.removeTab(tab); +} + +add_task(async function test_iframe() { + for (let enabled of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.disallowCrossSiteRelaxingDefault", enabled]], + }); + + await runTestIniFrame(gBrowser, enabled); + } +}); + +add_task(async function test_iframe_pbmode() { + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + for (let enabled of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.http.referer.disallowCrossSiteRelaxingDefault.pbmode", + enabled, + ], + ], + }); + + await runTestIniFrame(win.gBrowser, enabled); + } + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_link_click() { + for (let enabled of [true, false]) { + for (let enabled_top of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.http.referer.disallowCrossSiteRelaxingDefault", enabled], + [ + "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation", + enabled_top, + ], + ], + }); + + // We won't show the console message if the strict rule is disabled for + // the top navigation. + await runTestForLinkClick(gBrowser, enabled && enabled_top, !enabled_top); + } + } +}); + +add_task(async function test_link_click_pbmode() { + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + for (let enabled of [true, false]) { + for (let enabled_top of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.http.referer.disallowCrossSiteRelaxingDefault.pbmode", + enabled, + ], + [ + "network.http.referer.disallowCrossSiteRelaxingDefault.pbmode.top_navigation", + enabled_top, + ], + // Disable https first mode for private browsing mode to test downgrade + // cases. + ["dom.security.https_first_pbm", false], + ], + }); + + // We won't show the console message if the strict rule is disabled for + // the top navigation in the private browsing window. + await runTestForLinkClick( + win.gBrowser, + enabled && enabled_top, + !enabled_top + ); + } + } + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_iframe_etp_toggle_off() { + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.disallowCrossSiteRelaxingDefault", true]], + }); + + // Open a new tab for the test page and toggle ETP off. + await toggleETPForPage(gBrowser, TEST_PAGE, false); + + // Run the test to see if the protection is disabled. We won't send console + // message if the protection was disabled by the ETP toggle. + await runTestIniFrame(gBrowser, false, true); + + // toggle ETP on again. + await toggleETPForPage(gBrowser, TEST_PAGE, true); + + // Run the test again to see if the protection is enabled. + await runTestIniFrame(gBrowser, true); +}); + +add_task(async function test_link_click_etp_toggle_off() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.http.referer.disallowCrossSiteRelaxingDefault", true], + [ + "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation", + true, + ], + ], + }); + + // Toggle ETP off for the cross site. Note that the cross site is the place + // where we test against the ETP permission for top navigation. + await toggleETPForPage(gBrowser, TEST_CROSS_SITE_PAGE, false); + + // Run the test to see if the protection is disabled. We won't send console + // message if the protection was disabled by the ETP toggle. + await runTestForLinkClick(gBrowser, false, true); + + // toggle ETP on again. + await toggleETPForPage(gBrowser, TEST_CROSS_SITE_PAGE, true); + + // Run the test again to see if the protection is enabled. + await runTestForLinkClick(gBrowser, true); +}); diff --git a/dom/security/test/referrer-policy/browser_referrer_telemetry.js b/dom/security/test/referrer-policy/browser_referrer_telemetry.js new file mode 100644 index 0000000000..409c04c45e --- /dev/null +++ b/dom/security/test/referrer-policy/browser_referrer_telemetry.js @@ -0,0 +1,126 @@ +/** + * Bug 1720869 - Testing the referrer policy telemetry. + */ + +"use strict"; + +const TEST_DOMAIN = "https://example.com/"; +const TEST_CROSS_SITE_DOMAIN = "https://test1.example.org/"; + +const TEST_PATH = "browser/dom/security/test/referrer-policy/"; + +const TEST_PAGE = `${TEST_DOMAIN}${TEST_PATH}referrer_page.sjs`; +const TEST_CROSS_SITE_PAGE = `${TEST_CROSS_SITE_DOMAIN}${TEST_PATH}referrer_page.sjs`; + +// This matches to the order in ReferrerPolicy.webidl +const REFERRER_POLICY_INDEX = { + empty: 0, + "no-referrer": 1, + "no-referrer-when-downgrade": 2, + origin: 3, + "origin-when-cross-origin": 4, + "unsafe-url": 5, + "same-origin": 6, + "strict-origin": 7, + "strict-origin-when-cross-origin": 8, +}; + +const TEST_CASES = [ + { + policy: "", + expected: REFERRER_POLICY_INDEX.empty, + }, + { + policy: "no-referrer", + expected: REFERRER_POLICY_INDEX["no-referrer"], + }, + { + policy: "no-referrer-when-downgrade", + expected: REFERRER_POLICY_INDEX["no-referrer-when-downgrade"], + }, + { + policy: "origin", + expected: REFERRER_POLICY_INDEX.origin, + }, + { + policy: "origin-when-cross-origin", + expected: REFERRER_POLICY_INDEX["origin-when-cross-origin"], + }, + { + policy: "same-origin", + expected: REFERRER_POLICY_INDEX["same-origin"], + }, + { + policy: "strict-origin", + expected: REFERRER_POLICY_INDEX["strict-origin"], + }, + { + policy: "strict-origin-when-cross-origin", + expected: REFERRER_POLICY_INDEX["strict-origin-when-cross-origin"], + }, + { + policy: "unsafe-url", + expected: REFERRER_POLICY_INDEX["unsafe-url"], + }, +]; + +function clearTelemetry() { + Services.telemetry.getSnapshotForHistograms("main", true /* clear */); + Services.telemetry.getHistogramById("REFERRER_POLICY_COUNT").clear(); +} + +add_setup(async function() { + // Clear Telemetry probes before testing. + clearTelemetry(); +}); + +function verifyTelemetry(expected, isSameSite) { + // The record of cross-site loads is placed in the second half of the + // telemetry. + const offset = isSameSite ? 0 : Object.keys(REFERRER_POLICY_INDEX).length; + + let histograms = Services.telemetry.getSnapshotForHistograms( + "main", + false /* clear */ + ).parent; + + let referrerPolicyCountProbe = histograms.REFERRER_POLICY_COUNT; + + ok(referrerPolicyCountProbe, "The telemetry probe has been recorded."); + is( + referrerPolicyCountProbe.values[expected + offset], + 1, + "The telemetry is added correctly." + ); +} + +add_task(async function run_tests() { + for (let test of TEST_CASES) { + for (let sameSite of [true, false]) { + clearTelemetry(); + let referrerURL = `${TEST_PAGE}?header=${test.policy}`; + + await BrowserTestUtils.withNewTab(referrerURL, async browser => { + let iframeURL = sameSite + ? TEST_PAGE + "?show" + : TEST_CROSS_SITE_PAGE + "?show"; + + // Create an iframe and load the url. + await SpecialPowers.spawn(browser, [iframeURL], async url => { + let iframe = content.document.createElement("iframe"); + iframe.src = url; + + await new content.Promise(resolve => { + iframe.onload = () => { + resolve(); + }; + + content.document.body.appendChild(iframe); + }); + }); + + verifyTelemetry(test.expected, sameSite); + }); + } + } +}); diff --git a/dom/security/test/referrer-policy/img_referrer_testserver.sjs b/dom/security/test/referrer-policy/img_referrer_testserver.sjs new file mode 100644 index 0000000000..7fcc8d4914 --- /dev/null +++ b/dom/security/test/referrer-policy/img_referrer_testserver.sjs @@ -0,0 +1,337 @@ +var BASE_URL = + "example.com/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs"; +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +function createTestUrl(aPolicy, aAction, aName, aContent) { + var content = aContent || "text"; + return ( + "http://" + + BASE_URL + + "?" + + "action=" + + aAction + + "&" + + "policy=" + + aPolicy + + "&" + + "name=" + + aName + + "&" + + "content=" + + content + ); +} + +function createTestPage(aHead, aImgPolicy, aName) { + var _createTestUrl = createTestUrl.bind(null, aImgPolicy, "test", aName); + + return ( + "<!DOCTYPE HTML>\n\ + <html>" + + aHead + + '<body>\n\ + <img src="' + + _createTestUrl("img") + + '" referrerpolicy="' + + aImgPolicy + + '" id="image"></img>\n\ + <script>' + + // LOAD EVENT (of the test) + // fires when the img resource for the page is loaded + 'window.addEventListener("load", function() {\n\ + parent.postMessage("childLoadComplete", "http://mochi.test:8888");\n\ + }.bind(window), false);' + + "</script>\n\ + </body>\n\ + </html>" + ); +} + +// Creates the following test cases for the specified referrer +// policy combination: +// <img> with referrer +function createTest(aPolicy, aImgPolicy, aName) { + var headString = "<head>"; + if (aPolicy) { + headString += '<meta name="referrer" content="' + aPolicy + '">'; + } + + headString += "<script></script>"; + + return createTestPage(headString, aImgPolicy, aName); +} + +// testing regular load img with referrer policy +// speculative parser should not kick in here +function createTest2(aImgPolicy, name) { + return createTestPage("", aImgPolicy, name); +} + +function createTest3(aImgPolicy1, aImgPolicy2, aImgPolicy3, aName) { + return ( + '<!DOCTYPE HTML>\n\ + <html>\n\ + <body>\n\ + <img src="' + + createTestUrl(aImgPolicy1, "test", aName + aImgPolicy1) + + '" referrerpolicy="' + + aImgPolicy1 + + '" id="image"></img>\n\ + <img src="' + + createTestUrl(aImgPolicy2, "test", aName + aImgPolicy2) + + '" referrerpolicy="' + + aImgPolicy2 + + '" id="image"></img>\n\ + <img src="' + + createTestUrl(aImgPolicy3, "test", aName + aImgPolicy3) + + '" referrerpolicy="' + + aImgPolicy3 + + '" id="image"></img>\n\ + <script>\n\ + var _numLoads = 0;' + + // LOAD EVENT (of the test) + // fires when the img resource for the page is loaded + 'window.addEventListener("load", function() {\n\ + parent.postMessage("childLoadComplete", "http://mochi.test:8888");\n\ + }.bind(window), false);' + + "</script>\n\ + </body>\n\ + </html>" + ); +} + +function createTestPage2(aHead, aPolicy, aName) { + return ( + "<!DOCTYPE HTML>\n\ + <html>" + + aHead + + '<body>\n\ + <img src="' + + createTestUrl(aPolicy, "test", aName) + + '" id="image"></img>\n\ + <script>' + + // LOAD EVENT (of the test) + // fires when the img resource for the page is loaded + 'window.addEventListener("load", function() {\n\ + parent.postMessage("childLoadComplete", "http://mochi.test:8888");\n\ + }.bind(window), false);' + + "</script>\n\ + </body>\n\ + </html>" + ); +} + +function createTestPage3(aHead, aPolicy, aName) { + return ( + "<!DOCTYPE HTML>\n\ + <html>" + + aHead + + "<body>\n\ + <script>" + + 'var image = new Image();\n\ + image.src = "' + + createTestUrl(aPolicy, "test", aName, "image") + + '";\n\ + image.referrerPolicy = "' + + aPolicy + + '";\n\ + image.onload = function() {\n\ + window.parent.postMessage("childLoadComplete", "http://mochi.test:8888");\n\ + }\n\ + document.body.appendChild(image);' + + "</script>\n\ + </body>\n\ + </html>" + ); +} + +function createTestPage4(aHead, aPolicy, aName) { + return ( + "<!DOCTYPE HTML>\n\ + <html>" + + aHead + + "<body>\n\ + <script>" + + 'var image = new Image();\n\ + image.referrerPolicy = "' + + aPolicy + + '";\n\ + image.src = "' + + createTestUrl(aPolicy, "test", aName, "image") + + '";\n\ + image.onload = function() {\n\ + window.parent.postMessage("childLoadComplete", "http://mochi.test:8888");\n\ + }\n\ + document.body.appendChild(image);' + + "</script>\n\ + </body>\n\ + </html>" + ); +} + +function createSetAttributeTest1(aPolicy, aImgPolicy, aName) { + var headString = "<head>"; + headString += '<meta name="referrer" content="' + aPolicy + '">'; + headString += "<script></script>"; + + return createTestPage3(headString, aImgPolicy, aName); +} + +function createSetAttributeTest2(aPolicy, aImgPolicy, aName) { + var headString = "<head>"; + headString += '<meta name="referrer" content="' + aPolicy + '">'; + headString += "<script></script>"; + + return createTestPage4(headString, aImgPolicy, aName); +} + +function createTest4(aPolicy, aName) { + var headString = "<head>"; + headString += '<meta name="referrer" content="' + aPolicy + '">'; + headString += "<script></script>"; + + return createTestPage2(headString, aPolicy, aName); +} + +function createTest5(aPolicy, aName) { + var headString = "<head>"; + headString += '<meta name="referrer" content="' + aPolicy + '">'; + + return createTestPage2(headString, aPolicy, aName); +} + +function handleRequest(request, response) { + var sharedKey = "img_referrer_testserver.sjs"; + var params = request.queryString.split("&"); + var action = params[0].split("=")[1]; + + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + + if (action === "resetState") { + let state = getSharedState(sharedKey); + state = {}; + setSharedState(sharedKey, JSON.stringify(state)); + response.write(""); + return; + } + if (action === "test") { + // ?action=test&policy=origin&name=name&content=content + let policy = params[1].split("=")[1]; + let name = params[2].split("=")[1]; + let content = params[3].split("=")[1]; + let result = getSharedState(sharedKey); + + if (result === "") { + result = {}; + } else { + result = JSON.parse(result); + } + + if (!result.tests) { + result.tests = {}; + } + + var referrerLevel = "none"; + var test = {}; + if (request.hasHeader("Referer")) { + let referrer = request.getHeader("Referer"); + if (referrer.indexOf("img_referrer_testserver") > 0) { + referrerLevel = "full"; + } else if (referrer == "http://mochi.test:8888/") { + referrerLevel = "origin"; + } + test.referrer = request.getHeader("Referer"); + } else { + test.referrer = ""; + } + test.policy = referrerLevel; + test.expected = policy; + + result.tests[name] = test; + + setSharedState(sharedKey, JSON.stringify(result)); + + if (content === "image") { + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + } + return; + } + if (action === "get-test-results") { + // ?action=get-result + response.write(getSharedState(sharedKey)); + return; + } + if (action === "generate-img-policy-test") { + // ?action=generate-img-policy-test&imgPolicy=b64-encoded-string&name=name&policy=b64-encoded-string + let imgPolicy = unescape(params[1].split("=")[1]); + let name = unescape(params[2].split("=")[1]); + let metaPolicy = ""; + if (params[3]) { + metaPolicy = params[3].split("=")[1]; + } + + response.write(createTest(metaPolicy, imgPolicy, name)); + return; + } + if (action === "generate-img-policy-test2") { + // ?action=generate-img-policy-test2&imgPolicy=b64-encoded-string&name=name + let imgPolicy = unescape(params[1].split("=")[1]); + let name = unescape(params[2].split("=")[1]); + + response.write(createTest2(imgPolicy, name)); + return; + } + if (action === "generate-img-policy-test3") { + // ?action=generate-img-policy-test3&imgPolicy1=b64-encoded-string&imgPolicy2=b64-encoded-string&imgPolicy3=b64-encoded-string&name=name + let imgPolicy1 = unescape(params[1].split("=")[1]); + let imgPolicy2 = unescape(params[2].split("=")[1]); + let imgPolicy3 = unescape(params[3].split("=")[1]); + let name = unescape(params[4].split("=")[1]); + + response.write(createTest3(imgPolicy1, imgPolicy2, imgPolicy3, name)); + return; + } + if (action === "generate-img-policy-test4") { + // ?action=generate-img-policy-test4&imgPolicy=b64-encoded-string&name=name + let policy = unescape(params[1].split("=")[1]); + let name = unescape(params[2].split("=")[1]); + + response.write(createTest4(policy, name)); + return; + } + if (action === "generate-img-policy-test5") { + // ?action=generate-img-policy-test5&policy=b64-encoded-string&name=name + let policy = unescape(params[1].split("=")[1]); + let name = unescape(params[2].split("=")[1]); + + response.write(createTest5(policy, name)); + return; + } + + if (action === "generate-setAttribute-test1") { + // ?action=generate-setAttribute-test1&policy=b64-encoded-string&name=name + let imgPolicy = unescape(params[1].split("=")[1]); + let policy = unescape(params[2].split("=")[1]); + let name = unescape(params[3].split("=")[1]); + + response.write(createSetAttributeTest1(policy, imgPolicy, name)); + return; + } + + if (action === "generate-setAttribute-test2") { + // ?action=generate-setAttribute-test2&policy=b64-encoded-string&name=name + let imgPolicy = unescape(params[1].split("=")[1]); + let policy = unescape(params[2].split("=")[1]); + let name = unescape(params[3].split("=")[1]); + + response.write(createSetAttributeTest2(policy, imgPolicy, name)); + return; + } + + response.write("I don't know action " + action); +} diff --git a/dom/security/test/referrer-policy/mochitest.ini b/dom/security/test/referrer-policy/mochitest.ini new file mode 100644 index 0000000000..a873a3aeae --- /dev/null +++ b/dom/security/test/referrer-policy/mochitest.ini @@ -0,0 +1,12 @@ +[DEFAULT] +support-files = + img_referrer_testserver.sjs + referrer_header.sjs + referrer_header_current_document_iframe.html + referrer_helper.js + referrer_testserver.sjs + +[test_img_referrer.html] +[test_referrer_header_current_document.html] +[test_referrer_redirect.html] +# Please keep alphabetical order. diff --git a/dom/security/test/referrer-policy/referrer_header.sjs b/dom/security/test/referrer-policy/referrer_header.sjs new file mode 100644 index 0000000000..29c324b8f6 --- /dev/null +++ b/dom/security/test/referrer-policy/referrer_header.sjs @@ -0,0 +1,6 @@ +function handleRequest(request, response) { + response.setHeader("Referrer-Policy", "same-origin"); + response.write( + '<!DOCTYPE HTML><html><body>Loaded</body><script>parent.postMessage(document.referrer, "*");</script></html>' + ); +} diff --git a/dom/security/test/referrer-policy/referrer_header_current_document_iframe.html b/dom/security/test/referrer-policy/referrer_header_current_document_iframe.html new file mode 100644 index 0000000000..5996c8ba8a --- /dev/null +++ b/dom/security/test/referrer-policy/referrer_header_current_document_iframe.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<head> + <script> + window.addEventListener("load", function() { + document.getElementById("link").click(); + }); + </script> +</head> +<body> + <a id="link" href="http://example.org/tests/dom/security/test/referrer-policy/referrer_header.sjs">Navigate</a> +</body> diff --git a/dom/security/test/referrer-policy/referrer_helper.js b/dom/security/test/referrer-policy/referrer_helper.js new file mode 100644 index 0000000000..d202ea7836 --- /dev/null +++ b/dom/security/test/referrer-policy/referrer_helper.js @@ -0,0 +1,133 @@ +// This helper expects these globals to be defined. +/* global PARAMS, SJS, testCases */ + +/* + * common functionality for iframe, anchor, and area referrer attribute tests + */ +const GET_RESULT = SJS + "ACTION=get-test-results"; +const RESET_STATE = SJS + "ACTION=resetState"; + +SimpleTest.waitForExplicitFinish(); +var advance = function() { + tests.next(); +}; + +/** + * Listen for notifications from the child. + * These are sent in case of error, or when the loads we await have completed. + */ +window.addEventListener("message", function(event) { + if (event.data == "childLoadComplete") { + // all loads happen, continue the test. + advance(); + } +}); + +/** + * helper to perform an XHR + * to do checkIndividualResults and resetState + */ +function doXHR(aUrl, onSuccess, onFail) { + // The server is at http[s]://example.com so we need cross-origin XHR. + var xhr = new XMLHttpRequest({ mozSystem: true }); + xhr.responseType = "json"; + xhr.onload = function() { + onSuccess(xhr); + }; + xhr.onerror = function() { + onFail(xhr); + }; + xhr.open("GET", "http" + aUrl, true); + xhr.send(null); +} + +/** + * Grabs the results via XHR and passes to checker. + */ +function checkIndividualResults(aTestname, aExpectedReferrer, aName) { + var onload = xhr => { + var results = xhr.response; + info(JSON.stringify(xhr.response)); + ok(aName in results, aName + " tests have to be performed."); + is( + results[aName].policy, + aExpectedReferrer, + aTestname + + " --- " + + results[aName].policy + + " (" + + results[aName].referrer + + ")" + ); + advance(); + }; + var onerror = xhr => { + ok(false, "Can't get results from the counter server."); + SimpleTest.finish(); + }; + doXHR(GET_RESULT, onload, onerror); +} + +function resetState() { + doXHR(RESET_STATE, advance, function(xhr) { + ok(false, "error in reset state"); + SimpleTest.finish(); + }); +} + +/** + * testing if referrer header is sent correctly + */ +var tests = (function*() { + yield SpecialPowers.pushPrefEnv( + { set: [["network.preload", true]] }, + advance + ); + yield SpecialPowers.pushPrefEnv( + { set: [["security.mixed_content.block_active_content", false]] }, + advance + ); + yield SpecialPowers.pushPrefEnv( + { set: [["network.http.referer.disallowCrossSiteRelaxingDefault", false]] }, + advance + ); + yield SpecialPowers.pushPermissions( + [{ type: "systemXHR", allow: true, context: document }], + advance + ); + + var iframe = document.getElementById("testframe"); + + for (var j = 0; j < testCases.length; j++) { + if (testCases[j].PREFS) { + yield SpecialPowers.pushPrefEnv({ set: testCases[j].PREFS }, advance); + } + + var actions = testCases[j].ACTION; + var subTests = testCases[j].TESTS; + for (var k = 0; k < actions.length; k++) { + var actionString = actions[k]; + for (var i = 0; i < subTests.length; i++) { + yield resetState(); + var searchParams = new URLSearchParams(); + searchParams.append("ACTION", actionString); + searchParams.append("NAME", subTests[i].NAME); + for (var l of PARAMS) { + if (subTests[i][l]) { + searchParams.append(l, subTests[i][l]); + } + } + var schemeFrom = subTests[i].SCHEME_FROM || "http"; + yield (iframe.src = schemeFrom + SJS + searchParams.toString()); + yield checkIndividualResults( + subTests[i].DESC, + subTests[i].RESULT, + subTests[i].NAME + ); + } + } + } + + // complete. + SimpleTest.finish(); +})(); diff --git a/dom/security/test/referrer-policy/referrer_page.sjs b/dom/security/test/referrer-policy/referrer_page.sjs new file mode 100644 index 0000000000..62ec634e40 --- /dev/null +++ b/dom/security/test/referrer-policy/referrer_page.sjs @@ -0,0 +1,41 @@ +Cu.importGlobalProperties(["URLSearchParams"]); + +function handleRequest(request, response) { + let params = new URLSearchParams(request.queryString); + let referrerPolicyHeader = params.get("header") || ""; + let metaReferrerPolicy = params.get("meta") || ""; + let showReferrer = params.has("show"); + + if (referrerPolicyHeader) { + response.setHeader("Referrer-Policy", referrerPolicyHeader, false); + } + + let metaString = ""; + let resultString = ""; + + if (metaReferrerPolicy) { + metaString = `<meta name="referrer" content="${metaReferrerPolicy}">`; + } + + if (showReferrer) { + if (request.hasHeader("Referer")) { + resultString = `Referer Header: <a id="result">${request.getHeader( + "Referer" + )}</a>`; + } else { + resultString = `Referer Header: <a id="result"></a>`; + } + } + + response.write( + `<!DOCTYPE HTML> + <html> + <head> + ${metaString} + </head> + <body> + ${resultString} + </body> + </html>` + ); +} diff --git a/dom/security/test/referrer-policy/referrer_testserver.sjs b/dom/security/test/referrer-policy/referrer_testserver.sjs new file mode 100644 index 0000000000..10350b5da5 --- /dev/null +++ b/dom/security/test/referrer-policy/referrer_testserver.sjs @@ -0,0 +1,691 @@ +/* + * Test server for iframe, anchor, and area referrer attributes. + * https://bugzilla.mozilla.org/show_bug.cgi?id=1175736 + * Also server for further referrer tests such as redirecting tests + * bug 1174913, bug 1175736, bug 1184781 + */ + +Cu.importGlobalProperties(["URLSearchParams"]); +const SJS = "referrer_testserver.sjs?"; +const SJS_PATH = "/tests/dom/security/test/referrer-policy/"; +const BASE_ORIGIN = "example.com"; +const BASE_URL = BASE_ORIGIN + SJS_PATH + SJS; +const SHARED_KEY = SJS; +const SAME_ORIGIN = "mochi.test:8888" + SJS_PATH + SJS; +const CROSS_ORIGIN_URL = "test1.example.com" + SJS_PATH + SJS; + +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +function createTestUrl( + aPolicy, + aAction, + aName, + aType, + aSchemeFrom, + aSchemeTo, + crossOrigin, + referrerPolicyHeader +) { + var schemeTo = aSchemeTo || "http"; + var schemeFrom = aSchemeFrom || "http"; + var rpHeader = referrerPolicyHeader || ""; + var url = schemeTo + "://"; + url += crossOrigin ? CROSS_ORIGIN_URL : BASE_URL; + url += + "ACTION=" + + aAction + + "&" + + "policy=" + + aPolicy + + "&" + + "NAME=" + + aName + + "&" + + "type=" + + aType + + "&" + + "RP_HEADER=" + + rpHeader + + "&" + + "SCHEME_FROM=" + + schemeFrom; + return url; +} + +// test page using iframe referrer attribute +// if aParams are set this creates a test where the iframe url is a redirect +function createIframeTestPageUsingRefferer( + aMetaPolicy, + aAttributePolicy, + aNewAttributePolicy, + aName, + aParams, + aSchemeFrom, + aSchemeTo, + aChangingMethod +) { + var metaString = ""; + if (aMetaPolicy) { + metaString = `<meta name="referrer" content="${aMetaPolicy}">`; + } + var changeString = ""; + if (aChangingMethod === "setAttribute") { + changeString = `document.getElementById("myframe").setAttribute("referrerpolicy", "${aNewAttributePolicy}")`; + } else if (aChangingMethod === "property") { + changeString = `document.getElementById("myframe").referrerPolicy = "${aNewAttributePolicy}"`; + } + var iFrameString = `<iframe src="" id="myframe" ${ + aAttributePolicy ? ` referrerpolicy="${aAttributePolicy}"` : "" + }>iframe</iframe>`; + var iframeUrl = ""; + if (aParams) { + aParams.delete("ACTION"); + aParams.append("ACTION", "redirectIframe"); + iframeUrl = "http://" + CROSS_ORIGIN_URL + aParams.toString(); + } else { + iframeUrl = createTestUrl( + aAttributePolicy, + "test", + aName, + "iframe", + aSchemeFrom, + aSchemeTo + ); + } + + return `<!DOCTYPE HTML> + <html> + <head> + ${metaString} + </head> + <body> + ${iFrameString} + <script> + window.addEventListener("load", function() { + ${changeString} + document.getElementById("myframe").onload = function(){ + parent.postMessage("childLoadComplete", "http://mochi.test:8888"); + }; + document.getElementById("myframe").src = "${iframeUrl}"; + }.bind(window), false); + </script> + </body> + </html>`; +} + +function buildAnchorString( + aMetaPolicy, + aReferrerPolicy, + aName, + aRelString, + aSchemeFrom, + aSchemeTo +) { + if (aReferrerPolicy) { + return `<a href="${createTestUrl( + aReferrerPolicy, + "test", + aName, + "link", + aSchemeFrom, + aSchemeTo + )}" referrerpolicy="${aReferrerPolicy}" id="link" ${aRelString}>${aReferrerPolicy}</a>`; + } + return `<a href="${createTestUrl( + aMetaPolicy, + "test", + aName, + "link", + aSchemeFrom, + aSchemeTo + )}" id="link" ${aRelString}>link</a>`; +} + +function buildAreaString( + aMetaPolicy, + aReferrerPolicy, + aName, + aRelString, + aSchemeFrom, + aSchemeTo +) { + var result = `<img src="file_mozfiledataurl_img.jpg" alt="image" usemap="#imageMap">`; + result += `<map name="imageMap">`; + if (aReferrerPolicy) { + result += `<area shape="circle" coords="1,1,1" href="${createTestUrl( + aReferrerPolicy, + "test", + aName, + "link", + aSchemeFrom, + aSchemeTo + )}" alt="theArea" referrerpolicy="${aReferrerPolicy}" id="link" ${aRelString}>`; + } else { + result += `<area shape="circle" coords="1,1,1" href="${createTestUrl( + aMetaPolicy, + "test", + aName, + "link", + aSchemeFrom, + aSchemeTo + )}" alt="theArea" id="link" ${aRelString}>`; + } + result += `</map>`; + + return result; +} + +// test page using anchor or area referrer attribute +function createAETestPageUsingRefferer( + aMetaPolicy, + aAttributePolicy, + aNewAttributePolicy, + aName, + aRel, + aStringBuilder, + aSchemeFrom, + aSchemeTo, + aChangingMethod +) { + var metaString = ""; + if (aMetaPolicy) { + metaString = `<head><meta name="referrer" content="${aMetaPolicy}"></head>`; + } + var changeString = ""; + if (aChangingMethod === "setAttribute") { + changeString = `document.getElementById("link").setAttribute("referrerpolicy", "${aNewAttributePolicy}")`; + } else if (aChangingMethod === "property") { + changeString = `document.getElementById("link").referrerPolicy = "${aNewAttributePolicy}"`; + } + var relString = ""; + if (aRel) { + relString = `rel="noreferrer"`; + } + var elementString = aStringBuilder( + aMetaPolicy, + aAttributePolicy, + aName, + relString, + aSchemeFrom, + aSchemeTo + ); + + return `<!DOCTYPE HTML> + <html> + ${metaString} + <body> + ${elementString} + <script> + window.addEventListener("load", function() { + ${changeString} + document.getElementById("link").click(); + }.bind(window), false); + </script> + </body> + </html>`; +} + +// test page using anchor target=_blank rel=noopener +function createTargetBlankRefferer( + aMetaPolicy, + aName, + aSchemeFrom, + aSchemeTo, + aRpHeader +) { + var metaString = ""; + if (aMetaPolicy) { + metaString = `<head><meta name="referrer" content="${aMetaPolicy}"></head>`; + } + var elementString = `<a href="${createTestUrl( + aMetaPolicy, + "test", + aName, + "link", + aSchemeFrom, + aSchemeTo, + aRpHeader + )}" target=_blank rel="noopener" id="link">link</a>`; + + return `<!DOCTYPE HTML> + <html> + ${metaString} + <body> + ${elementString} + <script> + window.addEventListener("load", function() { + let link = document.getElementById("link"); + SpecialPowers.wrap(window).parent.postMessage("childLoadReady", "*"); + link.click(); + }.bind(window), false); + </script> + </body> + </html>`; +} + +// creates test page with img that is a redirect +function createRedirectImgTestCase(aParams, aAttributePolicy) { + var metaString = ""; + if (aParams.has("META_POLICY")) { + metaString = `<meta name="referrer" content="${aParams.get( + "META_POLICY" + )}">`; + } + aParams.delete("ACTION"); + aParams.append("ACTION", "redirectImg"); + var imgUrl = "http://" + CROSS_ORIGIN_URL + aParams.toString(); + + return `<!DOCTYPE HTML> + <html> + <head> + <meta charset="utf-8"> + ${metaString} + <title>Test referrer policies on redirect (img)</title> + </head> + <body> + <img id="testImg" src="${imgUrl}" ${ + aAttributePolicy ? ` referrerpolicy="${aAttributePolicy}"` : "" + }> + <script> + window.addEventListener("load", function() { + parent.postMessage("childLoadComplete", "http://mochi.test:8888"); + }.bind(window), false); + </script> + </body> + </html>`; +} + +// test page using link referrer attribute +function createLinkPageUsingRefferer( + aMetaPolicy, + aAttributePolicy, + aNewAttributePolicy, + aName, + aRel, + aStringBuilder, + aSchemeFrom, + aSchemeTo, + aTestType +) { + var metaString = ""; + if (aMetaPolicy) { + metaString = `<meta name="referrer" content="${aMetaPolicy}">`; + } + + var changeString = ""; + var policy = aAttributePolicy ? aAttributePolicy : aMetaPolicy; + var elementString = aStringBuilder( + policy, + aName, + aRel, + aSchemeFrom, + aSchemeTo, + aTestType + ); + + if (aTestType === "setAttribute") { + changeString = `var link = document.getElementById("test_link"); + link.setAttribute("referrerpolicy", "${aNewAttributePolicy}"); + link.href = "${createTestUrl( + policy, + "test", + aName, + "link_element_" + aRel, + aSchemeFrom, + aSchemeTo + )}";`; + } else if (aTestType === "property") { + changeString = `var link = document.getElementById("test_link"); + link.referrerPolicy = "${aNewAttributePolicy}"; + link.href = "${createTestUrl( + policy, + "test", + aName, + "link_element_" + aRel, + aSchemeFrom, + aSchemeTo + )}";`; + } + + return `<!DOCTYPE HTML> + <html> + <head> + ${metaString} + </head> + <body> + ${elementString} + <script> + ${changeString} + </script> + </body> + </html>`; +} + +function createFetchUserControlRPTestCase( + aName, + aSchemeFrom, + aSchemeTo, + crossOrigin +) { + var srcUrl = createTestUrl( + "", + "test", + aName, + "fetch", + aSchemeFrom, + aSchemeTo, + crossOrigin + ); + + return `<!DOCTYPE HTML> + <html> + <head> + <meta charset="utf-8"> + <title>Test user control referrer policies</title> + </head> + <body> + <script> + fetch("${srcUrl}", {referrerPolicy: ""}).then(function (response) { + window.parent.postMessage("childLoadComplete", "http://mochi.test:8888"); + }); + </script> + </body> + </html>`; +} + +function buildLinkString( + aPolicy, + aName, + aRel, + aSchemeFrom, + aSchemeTo, + aTestType +) { + var href = ""; + var onChildComplete = `window.parent.postMessage("childLoadComplete", "http://mochi.test:8888");`; + var policy = ""; + var asString = ""; + var relString = ""; + + if (aRel) { + relString = `rel="${aRel}"`; + } + + if (aPolicy) { + policy = `referrerpolicy=${aPolicy}`; + } + + if (aRel == "preload") { + asString = 'as="image"'; + } + + if (!aTestType) { + href = `href=${createTestUrl( + aPolicy, + "test", + aName, + "link_element_" + aRel, + aSchemeFrom, + aSchemeTo + )}`; + } + + return `<link ${relString} ${href} ${policy} ${asString} id="test_link" onload='${onChildComplete}' onerror='${onChildComplete}'>`; +} +// eslint-disable-next-line complexity +function handleRequest(request, response) { + var params = new URLSearchParams(request.queryString); + var action = params.get("ACTION"); + var schemeFrom = params.get("SCHEME_FROM") || "http"; + var schemeTo = params.get("SCHEME_TO") || "http"; + var crossOrigin = params.get("CROSS_ORIGIN") || false; + var referrerPolicyHeader = params.get("RP_HEADER") || ""; + + response.setHeader("Access-Control-Allow-Origin", "*", false); + if (referrerPolicyHeader) { + response.setHeader("Referrer-Policy", referrerPolicyHeader, false); + } + + if (action === "resetState") { + setSharedState(SHARED_KEY, "{}"); + response.write(""); + return; + } + if (action === "get-test-results") { + // ?action=get-result + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + response.write(getSharedState(SHARED_KEY)); + return; + } + if (action === "redirect") { + response.write( + '<script>parent.postMessage("childLoadComplete", "http://mochi.test:8888");</script>' + ); + return; + } + if (action === "redirectImg") { + params.delete("ACTION"); + params.append("ACTION", "test"); + params.append("type", "img"); + // 302 found, 301 Moved Permanently, 303 See Other, 307 Temporary Redirect + response.setStatusLine("1.1", 302, "found"); + response.setHeader( + "Location", + "http://" + CROSS_ORIGIN_URL + params.toString(), + false + ); + return; + } + if (action === "redirectIframe") { + params.delete("ACTION"); + params.append("ACTION", "test"); + params.append("type", "iframe"); + // 302 found, 301 Moved Permanently, 303 See Other, 307 Temporary Redirect + response.setStatusLine("1.1", 302, "found"); + response.setHeader( + "Location", + "http://" + CROSS_ORIGIN_URL + params.toString(), + false + ); + return; + } + if (action === "test") { + // ?action=test&policy=origin&name=name + let policy = params.get("policy"); + let name = params.get("NAME"); + let type = params.get("type"); + let result = getSharedState(SHARED_KEY); + + result = result ? JSON.parse(result) : {}; + + var referrerLevel = "none"; + var test = {}; + if (request.hasHeader("Referer")) { + var referrer = request.getHeader("Referer"); + if (referrer.indexOf("referrer_testserver") > 0) { + referrerLevel = "full"; + } else if (referrer.indexOf(schemeFrom + "://example.com") == 0) { + referrerLevel = "origin"; + } else { + // this is never supposed to happen + referrerLevel = "other-origin"; + } + test.referrer = referrer; + } else { + test.referrer = ""; + } + test.policy = referrerLevel; + test.expected = policy; + + result[name] = test; + + setSharedState(SHARED_KEY, JSON.stringify(result)); + + if (type === "img" || type == "link_element_preload") { + // return image + response.setHeader("Content-Type", "image/png"); + response.write(IMG_BYTES); + return; + } + if (type === "iframe") { + // return iframe page + response.write("<html><body>I am the iframe</body></html>"); + return; + } + if (type === "link") { + // forward link click to redirect URL to finish test + var loc = "http://" + BASE_URL + "ACTION=redirect"; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", loc, false); + } + return; + } + + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + + // parse test arguments and start test + var attributePolicy = params.get("ATTRIBUTE_POLICY") || ""; + var newAttributePolicy = params.get("NEW_ATTRIBUTE_POLICY") || ""; + var metaPolicy = params.get("META_POLICY") || ""; + var rel = params.get("REL") || ""; + var name = params.get("NAME"); + + // anchor & area + var _getPage = createAETestPageUsingRefferer.bind( + null, + metaPolicy, + attributePolicy, + newAttributePolicy, + name, + rel + ); + var _getAnchorPage = _getPage.bind( + null, + buildAnchorString, + schemeFrom, + schemeTo + ); + var _getAreaPage = _getPage.bind(null, buildAreaString, schemeFrom, schemeTo); + + // aMetaPolicy, aAttributePolicy, aNewAttributePolicy, aName, aChangingMethod, aStringBuilder + if (action === "generate-anchor-policy-test") { + response.write(_getAnchorPage()); + return; + } + if (action === "generate-anchor-changing-policy-test-set-attribute") { + response.write(_getAnchorPage("setAttribute")); + return; + } + if (action === "generate-anchor-changing-policy-test-property") { + response.write(_getAnchorPage("property")); + return; + } + if (action === "generate-area-policy-test") { + response.write(_getAreaPage()); + return; + } + if (action === "generate-area-changing-policy-test-set-attribute") { + response.write(_getAreaPage("setAttribute")); + return; + } + if (action === "generate-area-changing-policy-test-property") { + response.write(_getAreaPage("property")); + return; + } + if (action === "generate-anchor-target-blank-policy-test") { + response.write( + createTargetBlankRefferer( + metaPolicy, + name, + schemeFrom, + schemeTo, + referrerPolicyHeader + ) + ); + return; + } + + // iframe + _getPage = createIframeTestPageUsingRefferer.bind( + null, + metaPolicy, + attributePolicy, + newAttributePolicy, + name, + "", + schemeFrom, + schemeTo + ); + + // aMetaPolicy, aAttributePolicy, aNewAttributePolicy, aName, aChangingMethod + if (action === "generate-iframe-policy-test") { + response.write(_getPage()); + return; + } + if (action === "generate-iframe-changing-policy-test-set-attribute") { + response.write(_getPage("setAttribute")); + return; + } + if (action === "generate-iframe-changing-policy-test-property") { + response.write(_getPage("property")); + return; + } + + // redirect tests with img and iframe + if (action === "generate-img-redirect-policy-test") { + response.write(createRedirectImgTestCase(params, attributePolicy)); + return; + } + if (action === "generate-iframe-redirect-policy-test") { + response.write( + createIframeTestPageUsingRefferer( + metaPolicy, + attributePolicy, + newAttributePolicy, + name, + params, + schemeFrom, + schemeTo + ) + ); + return; + } + + _getPage = createLinkPageUsingRefferer.bind( + null, + metaPolicy, + attributePolicy, + newAttributePolicy, + name, + rel + ); + var _getLinkPage = _getPage.bind(null, buildLinkString, schemeFrom, schemeTo); + + // link + if (action === "generate-link-policy-test") { + response.write(_getLinkPage()); + return; + } + if (action === "generate-link-policy-test-set-attribute") { + response.write(_getLinkPage("setAttribute")); + return; + } + if (action === "generate-link-policy-test-property") { + response.write(_getLinkPage("property")); + return; + } + + if (action === "generate-fetch-user-control-policy-test") { + response.write( + createFetchUserControlRPTestCase(name, schemeFrom, schemeTo, crossOrigin) + ); + return; + } + + response.write("I don't know action " + action); +} diff --git a/dom/security/test/referrer-policy/test_img_referrer.html b/dom/security/test/referrer-policy/test_img_referrer.html new file mode 100644 index 0000000000..fcc80929d2 --- /dev/null +++ b/dom/security/test/referrer-policy/test_img_referrer.html @@ -0,0 +1,190 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test img policy attribute for Bug 1166910</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +<!-- +Testing that img referrer attribute is honoured correctly +* Speculative parser loads (generate-img-policy-test) +* regular loads (generate-img-policy-test2) +* loading a single image multiple times with different policies (generate-img-policy-test3) +* testing setAttribute and .referrer (generate-setAttribute-test) +* regression tests that meta referrer is still working even if attribute referrers are enabled +https://bugzilla.mozilla.org/show_bug.cgi?id=1166910 +--> + +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +var advance = function() { tests.next(); }; + +/** + * Listen for notifications from the child. + * These are sent in case of error, or when the loads we await have completed. + */ +window.addEventListener("message", function(event) { + if (event.data == "childLoadComplete" || + event.data.contains("childLoadComplete")) { + advance(); + } +}); + +/** + * helper to perform an XHR. + */ +function doXHR(aUrl, onSuccess, onFail) { + var xhr = new XMLHttpRequest(); + xhr.responseType = "json"; + xhr.onload = function () { + onSuccess(xhr); + }; + xhr.onerror = function () { + onFail(xhr); + }; + xhr.open('GET', aUrl, true); + xhr.send(null); +} + +/** + * Grabs the results via XHR and passes to checker. + */ +function checkIndividualResults(aTestname, aExpectedImg, aName) { + doXHR('/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=get-test-results', + function(xhr) { + var results = xhr.response; + info(JSON.stringify(xhr.response)); + + for (let i in aName) { + ok(aName[i] in results.tests, aName[i] + " tests have to be performed."); + is(results.tests[aName[i]].policy, aExpectedImg[i], aTestname + ' --- ' + results.tests[aName[i]].policy + ' (' + results.tests[aName[i]].referrer + ')'); + } + + advance(); + }, + function(xhr) { + ok(false, "Can't get results from the counter server."); + SimpleTest.finish(); + }); +} + +function resetState() { + doXHR('/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=resetState', + advance, + function(xhr) { + ok(false, "error in reset state"); + SimpleTest.finish(); + }); +} + +/** + * testing if img referrer attribute is honoured (1165501) + */ +var tests = (function*() { + + yield SpecialPowers.pushPrefEnv( + { set: [["network.http.referer.disallowCrossSiteRelaxingDefault", false]] }, + advance + ); + + var iframe = document.getElementById("testframe"); + var sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-img-policy-test"; + + // setting img unsafe-url and meta origin - unsafe-url shall prevail (should use speculative load) + yield resetState(); + var name = 'unsaf-url-with-meta-in-origin'; + yield iframe.src = sjs + "&imgPolicy=" + escape('unsafe-url') + "&name=" + name + "&policy=" + escape('origin'); + yield checkIndividualResults("unsafe-url (img) with origin in meta", ["full"], [name]); + + // setting img no-referrer and meta default - no-referrer shall prevail (should use speculative load) + yield resetState(); + name = 'no-referrer-with-meta-in-origin'; + yield iframe.src = sjs + "&imgPolicy=" + escape('no-referrer')+ "&name=" + name + "&policy=" + escape('origin'); + yield checkIndividualResults("no-referrer (img) with default in meta", ["none"], [name]); + + // test referrer policy in regular load + yield resetState(); + sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-img-policy-test2"; + name = 'regular-load-unsafe-url'; + yield iframe.src = sjs + "&imgPolicy=" + escape('unsafe-url') + "&name=" + name; + yield checkIndividualResults("unsafe-url in img", ["full"], [name]); + + // test referrer policy in regular load with multiple images + var policies = ['unsafe-url', 'origin', 'no-referrer']; + var expected = ["full", "origin", "none"]; + yield resetState(); + sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-img-policy-test3"; + name = 'multiple-images-'+policies[0]+'-'+policies[1]+'-'+policies[2]; + yield iframe.src = sjs + "&imgPolicy1=" + escape(policies[0]) + "&imgPolicy2=" + escape(policies[1]) + "&imgPolicy3=" + escape(policies[2]) + "&name=" + name; + yield checkIndividualResults(policies[0]+", "+policies[1]+" and "+policies[2]+" in img", expected, [name+policies[0], name+policies[1], name+policies[2]]); + + policies = ['origin', 'no-referrer', 'unsafe-url']; + expected = ["origin", "none", "full"]; + yield resetState(); + sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-img-policy-test3"; + name = 'multiple-images-'+policies[0]+'-'+policies[1]+'-'+policies[2]; + yield iframe.src = sjs + "&imgPolicy1=" + escape(policies[0]) + "&imgPolicy2=" + escape(policies[1]) + "&imgPolicy3=" + escape(policies[2]) + "&name=" + name; + yield checkIndividualResults(policies[0]+", "+policies[1]+" and "+policies[2]+" in img", expected, [name+policies[0], name+policies[1], name+policies[2]]); + + policies = ['no-referrer', 'origin', 'unsafe-url']; + expected = ["none", "origin", "full"]; + yield resetState(); + sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-img-policy-test3"; + name = 'multiple-images-'+policies[0]+'-'+policies[1]+'-'+policies[2]; + yield iframe.src = sjs + "&imgPolicy1=" + escape(policies[0]) + "&imgPolicy2=" + escape(policies[1]) + "&imgPolicy3=" + escape(policies[2]) + "&name=" + name; + yield checkIndividualResults(policies[0]+", "+policies[1]+" and "+policies[2]+" in img", expected, [name+policies[0], name+policies[1], name+policies[2]]); + + // regression tests that meta referrer is still working even if attribute referrers are enabled + yield resetState(); + sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-img-policy-test4"; + name = 'regular-load-no-referrer-meta'; + yield iframe.src = sjs + "&policy=" + escape('no-referrer') + "&name=" + name; + yield checkIndividualResults("no-referrer in meta (no img referrer policy), speculative load", ["none"], [name]); + + yield resetState(); + sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-img-policy-test5"; + name = 'regular-load-no-referrer-meta'; + yield iframe.src = sjs + "&policy=" + escape('no-referrer') + "&name=" + name; + yield checkIndividualResults("no-referrer in meta (no img referrer policy), regular load", ["none"], [name]); + + //test setAttribute + yield resetState(); + sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-setAttribute-test1"; + name = 'set-referrer-policy-attribute-before-src'; + yield iframe.src = sjs + "&imgPolicy=" + escape('no-referrer') + "&policy=" + escape('unsafe-url') + "&name=" + name; + yield checkIndividualResults("no-referrer in img", ["none"], [name]); + + yield resetState(); + sjs = "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-setAttribute-test2"; + name = 'set-referrer-policy-attribute-after-src'; + yield iframe.src = sjs + "&imgPolicy=" + escape('no-referrer') + "&policy=" + escape('unsafe-url') + "&name=" + name; + yield checkIndividualResults("no-referrer in img", ["none"], [name]); + + yield resetState(); + sjs = + "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-setAttribute-test2"; + name = 'set-invalid-referrer-policy-attribute-before-src-invalid'; + yield iframe.src = sjs + "&imgPolicy=" + escape('invalid') + "&policy=" + escape('unsafe-url') + "&name=" + name; + yield checkIndividualResults("unsafe-url in meta, invalid in img", ["full"], [name]); + + yield resetState(); + sjs = + "/tests/dom/security/test/referrer-policy/img_referrer_testserver.sjs?action=generate-setAttribute-test2"; + name = 'set-invalid-referrer-policy-attribute-before-src-invalid'; + yield iframe.src = sjs + "&imgPolicy=" + escape('default') + "&policy=" + escape('unsafe-url') + "&name=" + name; + yield checkIndividualResults("unsafe-url in meta, default in img", ["full"], [name]); + + // complete. + SimpleTest.finish(); +})(); + +</script> +</head> + +<body onload="tests.next();"> + <iframe id="testframe"></iframe> + +</body> +</html> diff --git a/dom/security/test/referrer-policy/test_referrer_header_current_document.html b/dom/security/test/referrer-policy/test_referrer_header_current_document.html new file mode 100644 index 0000000000..27dc54a21f --- /dev/null +++ b/dom/security/test/referrer-policy/test_referrer_header_current_document.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test referrer header not affecting document.referrer for current document for Bug 1601743</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + + <!-- + Testing that navigating to a document with Referrer-Policy:same-origin doesn't affect + the value of document.referrer for that document. + https://bugzilla.mozilla.org/show_bug.cgi?id=1601743 + --> + + <script type="application/javascript"> + function getExpectedReferrer(referrer) { + let defaultPolicy = SpecialPowers.getIntPref("network.http.referer.defaultPolicy"); + SimpleTest.ok([2, 3].indexOf(defaultPolicy) > -1, "default referrer policy should be either strict-origin-when-cross-origin(2) or no-referrer-when-downgrade(3)"); + if (defaultPolicy == 2) { + return referrer.match(/https?:\/\/[^\/]+\/?/i)[0]; + } + return referrer; + } + const IFRAME_URL = `${location.origin}/tests/dom/security/test/referrer-policy/referrer_header_current_document_iframe.html`; + + SimpleTest.waitForExplicitFinish(); + window.addEventListener("message", (event) => { + SimpleTest.is(event.data, getExpectedReferrer(IFRAME_URL), "Must have the original iframe as the referrer!"); + SimpleTest.finish(); + }, { once: true }); + </script> +</head> + +<body> +<iframe src="referrer_header_current_document_iframe.html"></iframe> +</body> diff --git a/dom/security/test/referrer-policy/test_referrer_redirect.html b/dom/security/test/referrer-policy/test_referrer_redirect.html new file mode 100644 index 0000000000..e6bd82fced --- /dev/null +++ b/dom/security/test/referrer-policy/test_referrer_redirect.html @@ -0,0 +1,123 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test anchor and area policy attribute for Bug 1184781</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <!-- + Testing referrer headers after redirects. + https://bugzilla.mozilla.org/show_bug.cgi?id=1184781 + --> + + <script type="application/javascript"> + + const SJS = "://example.com/tests/dom/security/test/referrer-policy/referrer_testserver.sjs?"; + const PARAMS = ["ATTRIBUTE_POLICY", "NEW_ATTRIBUTE_POLICY", "META_POLICY", "RP_HEADER"]; + + const testCases = [ + {ACTION: ["generate-img-redirect-policy-test", "generate-iframe-redirect-policy-test"], + TESTS: [ + { + ATTRIBUTE_POLICY: "no-referrer", + NAME: "no-referrer-with-no-meta", + DESC: "no-referrer (img/iframe) with no meta", + RESULT: "none" + }, + { + ATTRIBUTE_POLICY: "origin", + NAME: "origin-with-no-meta", + DESC: "origin (img/iframe) with no meta", + RESULT: "origin" + }, + { + ATTRIBUTE_POLICY: "unsafe-url", + NAME: "unsafe-url-with-no-meta", + DESC: "unsafe-url (img/iframe) with no meta", + RESULT: "full" + }, + { + META_POLICY: "unsafe-url", + NAME: "unsafe-url-in-meta", + DESC: "unsafe-url in meta", + RESULT: "full" + }, + { + META_POLICY: "origin", + NAME: "origin-in-meta", + DESC: "origin in meta", + RESULT: "origin" + }, + { + META_POLICY: "no-referrer", + NAME: "no-referrer-in-meta", + DESC: "no-referrer in meta", + RESULT: "none" + }, + { + META_POLICY: "origin-when-cross-origin", + NAME: "origin-when-cross-origin-in-meta", + DESC: "origin-when-cross-origin in meta", + RESULT: "origin" + }, + { + ATTRIBUTE_POLICY: "no-referrer", + RP_HEADER: "origin", + NAME: "no-referrer-with-no-meta-origin-RP-header", + DESC: "no-referrer (img/iframe) with no meta, origin Referrer-Policy redirect header", + RESULT: "none" + }, + { + ATTRIBUTE_POLICY: "origin", + RP_HEADER: "no-referrer", + NAME: "origin-with-no-meta-no-referrer-RP-header", + DESC: "origin (img/iframe) with no meta, no-referrer Referrer-Policy redirect header", + RESULT: "none" + }, + { + ATTRIBUTE_POLICY: "unsafe-url", + RP_HEADER: "origin", + NAME: "unsafe-url-with-no-meta-origin-RP-header", + DESC: "unsafe-url (img/iframe) with no meta, origin Referrer-Policy redirect header", + RESULT: "origin" + }, + { + META_POLICY: "unsafe-url", + RP_HEADER: "origin", + NAME: "unsafe-url-in-meta-origin-RP-header", + DESC: "unsafe-url in meta, origin Referrer-Policy redirect header", + RESULT: "origin" + }, + { + META_POLICY: "origin", + RP_HEADER: "no-referrer", + NAME: "origin-in-meta-no-referrer-RP-header", + DESC: "origin in meta, no-referrer Referrer-Policy redirect header", + RESULT: "none" + }, + { + META_POLICY: "no-referrer", + RP_HEADER: "origin", + NAME: "no-referrer-in-meta-origin-RP-header", + DESC: "no-referrer in meta, origin Referrer-Policy redirect header", + RESULT: "none" + }, + { + META_POLICY: "origin-when-cross-origin", + RP_HEADER: "unsafe-url", + NAME: "origin-when-cross-origin-in-meta-unsafe-url-RP-header", + DESC: "origin-when-cross-origin in meta, unsafe-url Referrer-Policy redirect header", + RESULT: "origin" + } + ] + } + ]; + </script> + <script type="application/javascript" src="/tests/dom/security/test/referrer-policy/referrer_helper.js"></script> +</head> +<body onload="tests.next();"> + <iframe id="testframe"></iframe> +</body> +</html> + diff --git a/dom/security/test/sec-fetch/browser.ini b/dom/security/test/sec-fetch/browser.ini new file mode 100644 index 0000000000..ab9c0b1a7d --- /dev/null +++ b/dom/security/test/sec-fetch/browser.ini @@ -0,0 +1,10 @@ +[DEFAULT] +support-files = file_no_cache.sjs + +[browser_navigation.js] +[browser_external_loads.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +support-files = + file_dummy_link.html + file_dummy_link_location.html diff --git a/dom/security/test/sec-fetch/browser_external_loads.js b/dom/security/test/sec-fetch/browser_external_loads.js new file mode 100644 index 0000000000..ea2467d1fb --- /dev/null +++ b/dom/security/test/sec-fetch/browser_external_loads.js @@ -0,0 +1,158 @@ +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +var gExpectedHeader = {}; + +function checkSecFetchUser(subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (!channel.URI.spec.startsWith("https://example.com")) { + return; + } + + info(`testing headers for load of ${channel.URI.spec}`); + + const secFetchHeaders = [ + "sec-fetch-mode", + "sec-fetch-dest", + "sec-fetch-user", + "sec-fetch-site", + ]; + + secFetchHeaders.forEach(header => { + const expectedValue = gExpectedHeader[header]; + try { + is( + channel.getRequestHeader(header), + expectedValue, + `${header} is set to ${expectedValue}` + ); + } catch (e) { + if (expectedValue) { + ok(false, `${header} should be set`); + } else { + ok(true, `${header} should not be set`); + } + } + }); +} + +add_task(async function external_load() { + waitForExplicitFinish(); + Services.obs.addObserver(checkSecFetchUser, "http-on-stop-request"); + + // Simulate an external load in the *current* window with + // Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL and the system principal. + gExpectedHeader = { + "sec-fetch-site": "none", + "sec-fetch-mode": "navigate", + "sec-fetch-dest": "document", + "sec-fetch-user": "?1", + }; + + let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + window.browserDOMWindow.openURI( + makeURI(`${TEST_PATH}file_dummy_link.html`), + null, + Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW, + Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL, + Services.scriptSecurityManager.getSystemPrincipal() + ); + await loaded; + + // Open a link in a *new* window through the context menu. + gExpectedHeader = { + "sec-fetch-site": "same-origin", + "sec-fetch-mode": "navigate", + "sec-fetch-dest": "document", + "sec-fetch-user": "?1", + }; + + loaded = BrowserTestUtils.waitForNewWindow({ + url: `${TEST_PATH}file_dummy_link_location.html`, + }); + BrowserTestUtils.waitForEvent(document, "popupshown", false, event => { + document.getElementById("context-openlink").doCommand(); + event.target.hidePopup(); + return true; + }); + BrowserTestUtils.synthesizeMouseAtCenter( + "#dummylink", + { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser + ); + + let win = await loaded; + win.close(); + + // Simulate an external load in a *new* window with + // Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL and the system principal. + gExpectedHeader = { + "sec-fetch-site": "none", + "sec-fetch-mode": "navigate", + "sec-fetch-dest": "document", + "sec-fetch-user": "?1", + }; + + loaded = BrowserTestUtils.waitForNewWindow({ + url: "https://example.com/newwindow", + }); + window.browserDOMWindow.openURI( + makeURI("https://example.com/newwindow"), + null, + Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW, + Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL, + Services.scriptSecurityManager.getSystemPrincipal() + ); + win = await loaded; + win.close(); + + // Open a *new* window through window.open without user activation. + gExpectedHeader = { + "sec-fetch-site": "same-origin", + "sec-fetch-mode": "navigate", + "sec-fetch-dest": "document", + }; + + loaded = BrowserTestUtils.waitForNewWindow({ + url: "https://example.com/windowopen", + }); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.window.open( + "https://example.com/windowopen", + "_blank", + "height=500,width=500" + ); + }); + win = await loaded; + win.close(); + + // Open a *new* window through window.open with user activation. + gExpectedHeader = { + "sec-fetch-site": "same-origin", + "sec-fetch-mode": "navigate", + "sec-fetch-dest": "document", + "sec-fetch-user": "?1", + }; + + loaded = BrowserTestUtils.waitForNewWindow({ + url: "https://example.com/windowopen_withactivation", + }); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + content.document.notifyUserGestureActivation(); + content.window.open( + "https://example.com/windowopen_withactivation", + "_blank", + "height=500,width=500" + ); + content.document.clearUserGestureActivation(); + }); + win = await loaded; + win.close(); + + Services.obs.removeObserver(checkSecFetchUser, "http-on-stop-request"); + finish(); +}); diff --git a/dom/security/test/sec-fetch/browser_navigation.js b/dom/security/test/sec-fetch/browser_navigation.js new file mode 100644 index 0000000000..fbfb153768 --- /dev/null +++ b/dom/security/test/sec-fetch/browser_navigation.js @@ -0,0 +1,186 @@ +"use strict"; + +const REQUEST_URL = + "https://example.com/browser/dom/security/test/sec-fetch/file_no_cache.sjs"; + +let gTestCounter = 0; +let gExpectedHeader = {}; + +async function setup() { + waitForExplicitFinish(); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.secFetch.enabled", true]], + }); +} + +function checkSecFetchUser(subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (!channel.URI.spec.startsWith("https://example.com/")) { + return; + } + + info(`testing headers for load of ${channel.URI.spec}`); + + const secFetchHeaders = [ + "sec-fetch-mode", + "sec-fetch-dest", + "sec-fetch-user", + "sec-fetch-site", + ]; + + secFetchHeaders.forEach(header => { + const expectedValue = gExpectedHeader[header]; + try { + is( + channel.getRequestHeader(header), + expectedValue, + `${header} is set to ${expectedValue}` + ); + } catch (e) { + if (expectedValue) { + ok(false, "required headers are set"); + } else { + ok(true, `${header} should not be set`); + } + } + }); + + gTestCounter++; +} + +async function testNavigations() { + gTestCounter = 0; + + // Load initial site + let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + BrowserTestUtils.loadURI(gBrowser, REQUEST_URL + "?test1"); + await loaded; + + // Load another site + loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + content.document.notifyUserGestureActivation(); // simulate user activation + let test2Button = content.document.getElementById("test2_button"); + test2Button.click(); + content.document.clearUserGestureActivation(); + }); + await loaded; + // Load another site + loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + content.document.notifyUserGestureActivation(); // simulate user activation + let test3Button = content.document.getElementById("test3_button"); + test3Button.click(); + content.document.clearUserGestureActivation(); + }); + await loaded; + + gExpectedHeader = { + "sec-fetch-mode": "navigate", + "sec-fetch-dest": "document", + "sec-fetch-site": "same-origin", + "sec-fetch-user": "?1", + }; + + // Register the http request observer. + // All following actions should cause requests with the sec-fetch-user header + // set. + Services.obs.addObserver(checkSecFetchUser, "http-on-stop-request"); + + // Go back one site by clicking the back button + info("Clicking back button"); + loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + document.notifyUserGestureActivation(); // simulate user activation + let backButton = document.getElementById("back-button"); + backButton.click(); + document.clearUserGestureActivation(); + await loaded; + + // Reload the site by clicking the reload button + info("Clicking reload button"); + loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + document.notifyUserGestureActivation(); // simulate user activation + let reloadButton = document.getElementById("reload-button"); + await TestUtils.waitForCondition(() => { + return !reloadButton.disabled; + }); + reloadButton.click(); + document.clearUserGestureActivation(); + await loaded; + + // Go forward one site by clicking the forward button + info("Clicking forward button"); + loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + document.notifyUserGestureActivation(); // simulate user activation + let forwardButton = document.getElementById("forward-button"); + forwardButton.click(); + document.clearUserGestureActivation(); + await loaded; + + // Testing history.back/forward... + + info("going back with history.back"); + loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + content.document.notifyUserGestureActivation(); // simulate user activation + content.history.back(); + content.document.clearUserGestureActivation(); + }); + await loaded; + + info("going forward with history.forward"); + loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + content.document.notifyUserGestureActivation(); // simulate user activation + content.history.forward(); + content.document.clearUserGestureActivation(); + }); + await loaded; + + gExpectedHeader = { + "sec-fetch-mode": "navigate", + "sec-fetch-dest": "document", + "sec-fetch-site": "same-origin", + }; + + info("going back with history.back without user activation"); + loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + content.history.back(); + }); + await loaded; + + info("going forward with history.forward without user activation"); + loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() { + content.history.forward(); + }); + await loaded; + + ok(gTestCounter === 7, "testing that all five actions have been tested."); + + Services.obs.removeObserver(checkSecFetchUser, "http-on-stop-request"); +} + +add_task(async function() { + waitForExplicitFinish(); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.secFetch.enabled", true]], + }); + + await testNavigations(); + + // If fission is enabled we also want to test the navigations with the bfcache + // in the parent. + if (SpecialPowers.getBoolPref("fission.autostart")) { + await SpecialPowers.pushPrefEnv({ + set: [["fission.bfcacheInParent", true]], + }); + + await testNavigations(); + } + + finish(); +}); diff --git a/dom/security/test/sec-fetch/file_dummy_link.html b/dom/security/test/sec-fetch/file_dummy_link.html new file mode 100644 index 0000000000..2150054226 --- /dev/null +++ b/dom/security/test/sec-fetch/file_dummy_link.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1738694 - Sec-Fetch-User header is missing when opening a link in a new window</title> +</head> +<body> + <a id="dummylink" href="file_dummy_link_location.html">Open</a> +</body> +</html> diff --git a/dom/security/test/sec-fetch/file_dummy_link_location.html b/dom/security/test/sec-fetch/file_dummy_link_location.html new file mode 100644 index 0000000000..9f9400e1c3 --- /dev/null +++ b/dom/security/test/sec-fetch/file_dummy_link_location.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1738694 - Sec-Fetch-User header is missing when opening a link in a new window</title> +</head> +<body> + <h1>file_dummy_link_location.html</h1> +</body> +</html> diff --git a/dom/security/test/sec-fetch/file_no_cache.sjs b/dom/security/test/sec-fetch/file_no_cache.sjs new file mode 100644 index 0000000000..f5f9779e80 --- /dev/null +++ b/dom/security/test/sec-fetch/file_no_cache.sjs @@ -0,0 +1,28 @@ +const MESSAGE_PAGE = function(msg) { + return ` +<html> +<script type="text/javascript"> +window.parent.postMessage({test : "${msg}"},"*"); +</script> +<script> + addEventListener("back", () => { + history.back(); + }); + addEventListener("forward", () => { + history.forward(); + }); +</script> +<body> + <a id="test2_button" href="https://example.com/browser/dom/security/test/sec-fetch/file_no_cache.sjs?test2">Click me</a> + <a id="test3_button" href="https://example.com/browser/dom/security/test/sec-fetch/file_no_cache.sjs?test3">Click me</a> +<body> +</html> +`; +}; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-store"); + response.setHeader("Content-Type", "text/html"); + + response.write(MESSAGE_PAGE(request.queryString)); +} diff --git a/dom/security/test/sec-fetch/file_redirect.sjs b/dom/security/test/sec-fetch/file_redirect.sjs new file mode 100644 index 0000000000..84c2c39913 --- /dev/null +++ b/dom/security/test/sec-fetch/file_redirect.sjs @@ -0,0 +1,34 @@ +const SITE_META_REDIRECT = ` +<html> + <head> + <meta http-equiv="refresh" content="0; url='https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs?redirect302'"> + </head> + <body> + META REDIRECT + </body> +</html> +`; + +const REDIRECT_302 = + "https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs?pageC"; + +function handleRequest(req, res) { + // avoid confusing cache behaviour + res.setHeader("Cache-Control", "no-cache", false); + res.setHeader("Content-Type", "text/html", false); + + switch (req.queryString) { + case "meta": + res.write(SITE_META_REDIRECT); + return; + case "redirect302": + res.setStatusLine("1.1", 302, "Found"); + res.setHeader("Location", REDIRECT_302, false); + return; + case "pageC": + res.write("<html><body>PAGE C</body></html>"); + return; + } + + res.write(`<html><body>THIS SHOULD NEVER BE DISPLAYED</body></html>`); +} diff --git a/dom/security/test/sec-fetch/file_trustworthy_loopback.html b/dom/security/test/sec-fetch/file_trustworthy_loopback.html new file mode 100644 index 0000000000..88f9242650 --- /dev/null +++ b/dom/security/test/sec-fetch/file_trustworthy_loopback.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1732069: Sec-Fetch-Site inconsistent on localhost/IPs</title> +</head> +<body> + <iframe src="http://localhost:9898/foo"></iframe> + <iframe src="http://localhost:9899/foo"></iframe> + <iframe src="http://sub.localhost/foo"></iframe> +</body> +</html> diff --git a/dom/security/test/sec-fetch/file_websocket_wsh.py b/dom/security/test/sec-fetch/file_websocket_wsh.py new file mode 100644 index 0000000000..b7159c742b --- /dev/null +++ b/dom/security/test/sec-fetch/file_websocket_wsh.py @@ -0,0 +1,6 @@ +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + pass diff --git a/dom/security/test/sec-fetch/mochitest.ini b/dom/security/test/sec-fetch/mochitest.ini new file mode 100644 index 0000000000..a142f210dd --- /dev/null +++ b/dom/security/test/sec-fetch/mochitest.ini @@ -0,0 +1,14 @@ +[DEFAULT] +support-files = + file_no_cache.sjs + file_redirect.sjs + +[test_websocket.html] +skip-if = toolkit == 'android' # no websocket support Bug 982828 +support-files = file_websocket_wsh.py +[test_iframe_src_metaRedirect.html] +[test_iframe_srcdoc_metaRedirect.html] +[test_iframe_window_open_metaRedirect.html] +[test_iframe_history_manipulation.html] +[test_trustworthy_loopback.html] +support-files = file_trustworthy_loopback.html diff --git a/dom/security/test/sec-fetch/test_iframe_history_manipulation.html b/dom/security/test/sec-fetch/test_iframe_history_manipulation.html new file mode 100644 index 0000000000..5d89c3c04a --- /dev/null +++ b/dom/security/test/sec-fetch/test_iframe_history_manipulation.html @@ -0,0 +1,88 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1648825 - Fetch Metadata Headers contain invalid value for Sec-Fetch-Site for history manipulation</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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 REQUEST_PATH = 'tests/dom/security/test/sec-fetch/file_no_cache.sjs' +let sendHome = true; +let testCounter = 0; +let testFrame; + +var script = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + Services.obs.addObserver(function onExamResp(subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + info("request observed: " + channel.URI.spec); + if (!channel.URI.spec.startsWith("https://example.org")) { + return; + } + let headerPresent = false; + try { + is(channel.getRequestHeader("Sec-Fetch-Site"), "cross-site", "testing sec-fetch-site is cross-site"); + + // This should fail and cause the catch clause to be executed. + channel.getRequestHeader("Sec-Fetch-User"); + headerPresent = true; + } catch (e) { + headerPresent = false; + } + + ok(!headerPresent, "testing sec-fetch-user header is not set"); + + sendAsyncMessage("test-pass"); + }, "http-on-stop-request"); +}); + +script.addMessageListener("test-pass", () => { + testCounter++; + if(testCounter == 2) { + SimpleTest.finish(); + } +}); + +window.addEventListener("message", function (event) { + iframeAction(event.data.test); +}); + +function iframeAction(test) { + info("received message " + test); + + switch (test) { + case 'test': + testFrame.contentWindow.location = `https://example.org/${REQUEST_PATH}?test#bypass`; + if(sendHome) { + // We need to send the message manually here because there is no request send to the server. + window.postMessage({test: "home"}, "*"); + sendHome = false; + } + + break; + case 'home': + testFrame.contentWindow.location = `/${REQUEST_PATH}?back`; + break; + case 'back': + testFrame.contentWindow.history.back(); + break; + } +} + +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.pushPrefEnv({set: [["dom.security.secFetch.enabled", true]]}, async () => { + testFrame = document.createElement('iframe'); + testFrame.src = `https://example.org/${REQUEST_PATH}?test`; + document.body.appendChild(testFrame); +}); + + +</script> +</body> +</html> diff --git a/dom/security/test/sec-fetch/test_iframe_src_metaRedirect.html b/dom/security/test/sec-fetch/test_iframe_src_metaRedirect.html new file mode 100644 index 0000000000..c58d95233e --- /dev/null +++ b/dom/security/test/sec-fetch/test_iframe_src_metaRedirect.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1647128 - Fetch Metadata Headers contain invalid value for Sec-Fetch-Site for meta redirects</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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"> +/* + * Description of the test: + * We load site A that redirects to site B using a meta refresh, + * finally site B redirects to site C via a 302 redirect. + * The first load of site A is made by an iframe: frame.src = "...". + * We check that all requests have the Sec-Fetch-* headers set appropriately. + */ + +SimpleTest.waitForExplicitFinish(); + +const REQUEST_URL = "https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs"; +let testPassCounter = 0; + +var script = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + Services.obs.addObserver(function onExamResp(subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (!channel.URI.spec.startsWith("https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs")) { + return; + } + + // The redirection flow is the following: + // http://mochi.test:8888 -> https://example.com?meta -> https://example.com?redirect302 -> https://example.com?pageC + // So the Sec-Fetch-* headers for each request should be: + const expectedHeaders = { + "?meta": { + "Sec-Fetch-Site": "cross-site", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Dest": "iframe", + "Sec-Fetch-User": null, + }, + "?redirect302": { + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Dest": "iframe", + "Sec-Fetch-User": null, + }, + "?pageC": { + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Dest": "iframe", + "Sec-Fetch-User": null, + }, + }; + + let matchedOne = false; + for (const [query, headers] of Object.entries(expectedHeaders)) { + if (!channel.URI.spec.endsWith(query)) { + continue; + } + matchedOne = true; + + for (const [header, value] of Object.entries(headers)) { + try { + is(channel.getRequestHeader(header), value, `testing ${header} for the ${query} query`); + } catch (e) { + is(header, "Sec-Fetch-User", "testing Sec-Fetch-User"); + } + } + } + ok(matchedOne, "testing expectedHeaders"); + + sendAsyncMessage("test-pass"); + }, "http-on-stop-request"); +}); + +script.addMessageListener("test-pass", async () => { + testPassCounter++; + if (testPassCounter < 3) { + return; + } + + // If we received "test-pass" 3 times we know that all loads had Sec-Fetch-* headers set appropriately. + SimpleTest.finish(); +}); + +SpecialPowers.pushPrefEnv({set: [["dom.security.secFetch.enabled", true]]}, async () => { + let frame = document.createElement("iframe"); + frame.src = REQUEST_URL + "?meta"; + document.body.appendChild(frame); +}); + +</script> +</body> +</html> diff --git a/dom/security/test/sec-fetch/test_iframe_srcdoc_metaRedirect.html b/dom/security/test/sec-fetch/test_iframe_srcdoc_metaRedirect.html new file mode 100644 index 0000000000..e44700541d --- /dev/null +++ b/dom/security/test/sec-fetch/test_iframe_srcdoc_metaRedirect.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1647128 - Fetch Metadata Headers contain invalid value for Sec-Fetch-Site for meta redirects</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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"> +/* + * Description of the test: + * We load site A that redirects to site B using a meta refresh, + * finally site B redirects to site C via a 302 redirect. + * The first load of site A is made by an iframe: frame.srcdoc = "<meta ...". + * We check that all requests have the Sec-Fetch-* headers set appropriately. + */ + +SimpleTest.waitForExplicitFinish(); + +const REQUEST_URL = "https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs"; +let testPassCounter = 0; + +var script = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + Services.obs.addObserver(function onExamResp(subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (!channel.URI.spec.startsWith("https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs")) { + return; + } + + // The redirection flow is the following: + // http://mochi.test:8888 -> https://example.com?meta -> https://example.com?redirect302 -> https://example.com?pageC + // So the Sec-Fetch-* headers for each request should be: + const expectedHeaders = { + "?meta": { + "Sec-Fetch-Site": "cross-site", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Dest": "iframe", + "Sec-Fetch-User": null, + }, + "?redirect302": { + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Dest": "iframe", + "Sec-Fetch-User": null, + }, + "?pageC": { + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Dest": "iframe", + "Sec-Fetch-User": null, + }, + }; + + let matchedOne = false; + for (const [query, headers] of Object.entries(expectedHeaders)) { + if (!channel.URI.spec.endsWith(query)) { + continue; + } + matchedOne = true; + + for (const [header, value] of Object.entries(headers)) { + try { + is(channel.getRequestHeader(header), value, `testing ${header} for the ${query} query`); + } catch (e) { + is(header, "Sec-Fetch-User", "testing Sec-Fetch-User"); + } + } + } + ok(matchedOne, "testing expectedHeaders"); + + sendAsyncMessage("test-pass"); + }, "http-on-stop-request"); +}); + +script.addMessageListener("test-pass", async () => { + testPassCounter++; + if (testPassCounter < 3) { + return; + } + + // If we received "test-pass" 3 times we know that all loads had Sec-Fetch-* headers set appropriately. + SimpleTest.finish(); +}); + +SpecialPowers.pushPrefEnv({set: [["dom.security.secFetch.enabled", true]]}, async () => { + let frame = document.createElement("iframe"); + frame.srcdoc = `<meta http-equiv="refresh" content="0; url='${REQUEST_URL}?meta'">`; + document.body.appendChild(frame); +}); + +</script> +</body> +</html> diff --git a/dom/security/test/sec-fetch/test_iframe_window_open_metaRedirect.html b/dom/security/test/sec-fetch/test_iframe_window_open_metaRedirect.html new file mode 100644 index 0000000000..b45ed25a6b --- /dev/null +++ b/dom/security/test/sec-fetch/test_iframe_window_open_metaRedirect.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1647128 - Fetch Metadata Headers contain invalid value for Sec-Fetch-Site for meta redirects</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script 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"> +/* + * Description of the test: + * We load site A that redirects to site B using a meta refresh, + * finally site B redirects to site C via a 302 redirect. + * The first load of site A is made through window.open. + * We check that all requests have the Sec-Fetch-* headers set appropriately. + */ + +SimpleTest.waitForExplicitFinish(); + +const REQUEST_URL = "https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs"; +let testPassCounter = 0; +let testWindow; + +var script = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + Services.obs.addObserver(function onExamResp(subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (!channel.URI.spec.startsWith("https://example.com/tests/dom/security/test/sec-fetch/file_redirect.sjs")) { + return; + } + + // The redirection flow is the following: + // http://mochi.test:8888 -> https://example.com?meta -> https://example.com?redirect302 -> https://example.com?pageC + // So the Sec-Fetch-* headers for each request should be: + const expectedHeaders = { + "?meta": { + "Sec-Fetch-Site": "cross-site", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-User": null, + }, + "?redirect302": { + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-User": null, + }, + "?pageC": { + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-User": null, + }, + }; + + let matchedOne = false; + for (const [query, headers] of Object.entries(expectedHeaders)) { + if (!channel.URI.spec.endsWith(query)) { + continue; + } + matchedOne = true; + + for (const [header, value] of Object.entries(headers)) { + try { + is(channel.getRequestHeader(header), value, `testing ${header} for the ${query} query`); + } catch (e) { + is(header, "Sec-Fetch-User", "testing Sec-Fetch-User"); + } + } + } + ok(matchedOne, "testing expectedHeaders"); + + sendAsyncMessage("test-pass"); + }, "http-on-stop-request"); +}); + +script.addMessageListener("test-pass", async () => { + testPassCounter++; + if (testPassCounter < 3) { + return; + } + + if (testWindow) { + testWindow.close(); + } + + // If we received "test-pass" 3 times we know that all loads had Sec-Fetch-* headers set appropriately. + SimpleTest.finish(); +}); + +SpecialPowers.pushPrefEnv({set: [["dom.security.secFetch.enabled", true]]}, async () => { + testWindow = window.open(REQUEST_URL + "?meta"); +}); + +</script> +</body> +</html> diff --git a/dom/security/test/sec-fetch/test_trustworthy_loopback.html b/dom/security/test/sec-fetch/test_trustworthy_loopback.html new file mode 100644 index 0000000000..95ecac17ed --- /dev/null +++ b/dom/security/test/sec-fetch/test_trustworthy_loopback.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1732069: Sec-Fetch-Site inconsistent on localhost/IPs</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +let testsSucceeded = 0; + +let win; +function checkTestsDone() { + testsSucceeded++; + if (testsSucceeded == 3) { + win.close(); + SimpleTest.finish(); + } +} + +var script = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + Services.obs.addObserver(function onExamResp(subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (!channel.URI.spec.includes("localhost") || + channel.URI.spec.startsWith("http://localhost:9898/tests/dom/security/test/sec-fetch/file_trustworthy_loopback.html")) { + return; + } + + const expectedHeaders = { + "localhost:9898": { + "sec-fetch-site": "same-origin", + "sec-fetch-mode": "navigate", + "sec-fetch-dest": "iframe", + }, + "sub.localhost:-1": { + "sec-fetch-site": "cross-site", + "sec-fetch-mode": "navigate", + "sec-fetch-dest": "iframe", + }, + "localhost:9899": { + "sec-fetch-site": "same-site", + "sec-fetch-mode": "navigate", + "sec-fetch-dest": "iframe", + }, + }; + + info(`checking headers for request to ${channel.URI.spec}`); + const expected = expectedHeaders[channel.URI.host + ":" + channel.URI.port]; + for (let key in expected) { + try { + is(channel.getRequestHeader(key), expected[key], `${key} header matches`); + } catch (e) { + ok(false, "failed to check headers"); + } + } + sendAsyncMessage("test-end"); + }, "http-on-stop-request"); +}); + +script.addMessageListener("test-end", () => { + checkTestsDone(); +}); + +SpecialPowers.pushPrefEnv({set: [ + ["network.proxy.allow_hijacking_localhost", true], + ["network.proxy.testing_localhost_is_secure_when_hijacked", true], +]}).then(function() { + win = window.open("http://localhost:9898/tests/dom/security/test/sec-fetch/file_trustworthy_loopback.html"); +}); + +</script> +</body> +</html> diff --git a/dom/security/test/sec-fetch/test_websocket.html b/dom/security/test/sec-fetch/test_websocket.html new file mode 100644 index 0000000000..640ccaaf70 --- /dev/null +++ b/dom/security/test/sec-fetch/test_websocket.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1628605: Test Sec-Fetch-* header for websockets</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +let testsSucceeded = 0; + +function checkTestsDone() { + testsSucceeded++; + if (testsSucceeded == 2) { + SimpleTest.finish(); + } +} + +var script = SpecialPowers.loadChromeScript(() => { + /* eslint-env mozilla/chrome-script */ + Services.obs.addObserver(function onExamResp(subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (!channel.URI.spec.startsWith("https://example.com/tests/dom/security/test/sec-fetch/file_websocket")) { + return; + } + + // Sec-Fetch-* Headers should be present for Dest, Mode, Site + try { + let secFetchDest = channel.getRequestHeader("Sec-Fetch-Dest"); + is(secFetchDest, "websocket", "testing sec-fetch-dest"); + + let secFetchMode = channel.getRequestHeader("Sec-Fetch-Mode"); + is(secFetchMode, "websocket", "testing sec-fetch-mode"); + + let secFetchSite = channel.getRequestHeader("Sec-Fetch-Site"); + is(secFetchSite, "cross-site", "testing sec-fetch-site"); + } + catch (e) { + ok(false, "testing sec-fetch-*"); + } + + // Sec-Fetch-User should not be present + try { + channel.getRequestHeader("Sec-Fetch-User"); + ok(false, "testing sec-fetch-user"); + } + catch (e) { + ok(true, "testing sec-fetch-user"); + } + Services.obs.removeObserver(onExamResp, "http-on-stop-request"); + + sendAsyncMessage("test-end"); + }, "http-on-stop-request"); +}); + +script.addMessageListener("test-end", () => { + checkTestsDone(); +}); + +function test_sec_fetch_websocket() { + var wssSocket = new WebSocket("wss://example.com/tests/dom/security/test/sec-fetch/file_websocket"); + wssSocket.onopen = function(e) { + ok(true, "sanity: wssSocket onopen"); + checkTestsDone(); + }; + wssSocket.onerror = function(e) { + ok(false, "sanity: wssSocket onerror"); + }; +} + +SpecialPowers.pushPrefEnv({ + set: [["dom.security.secFetch.enabled", true]] +}, test_sec_fetch_websocket); + +</script> +</body> +</html> 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 0000000000..c0928f2cf0 --- /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_script_crossdomain.html b/dom/security/test/sri/iframe_script_crossdomain.html new file mode 100644 index 0000000000..fe91834db5 --- /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 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 0000000000..8c1994fec4 --- /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 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_style_crossdomain.html b/dom/security/test/sri/iframe_style_crossdomain.html new file mode 100644 index 0000000000..f5eb57cbe7 --- /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 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).getPropertyValue('color'); + var greenTextColor = window.getComputedStyle(greenText).getPropertyValue('color'); + var blueTextColor = window.getComputedStyle(blueText).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 0000000000..52ebd10d9b --- /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 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).getPropertyValue('color'); + var blueTextColor = window.getComputedStyle(blueText).getPropertyValue('color'); + var blackTextColor1 = window.getComputedStyle(blackText1).getPropertyValue('color'); + var blackTextColor2 = window.getComputedStyle(blackText2).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 0000000000..1d6065e5aa --- /dev/null +++ b/dom/security/test/sri/mochitest.ini @@ -0,0 +1,44 @@ +[DEFAULT] +support-files = + file_bug_1271796.css + iframe_script_crossdomain.html + iframe_script_sameorigin.html + iframe_style_crossdomain.html + iframe_style_sameorigin.html + script_crossdomain1.js + script_crossdomain1.js^headers^ + script_crossdomain2.js + script_crossdomain3.js + script_crossdomain3.js^headers^ + script_crossdomain4.js + script_crossdomain4.js^headers^ + script_crossdomain5.js + script_crossdomain5.js^headers^ + script.js + script.js^headers^ + script_301.js + script_301.js^headers^ + script_302.js + script_302.js^headers^ + script_401.js + script_401.js^headers^ + style1.css + style1.css^headers^ + style2.css + style3.css + 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_style_crossdomain.html] +[test_style_sameorigin.html] +[test_bug_1271796.html] +[test_bug_1364262.html] diff --git a/dom/security/test/sri/script.js b/dom/security/test/sri/script.js new file mode 100644 index 0000000000..8fd8f96b2f --- /dev/null +++ b/dom/security/test/sri/script.js @@ -0,0 +1 @@ +var load=true; diff --git a/dom/security/test/sri/script.js^headers^ b/dom/security/test/sri/script.js^headers^ new file mode 100644 index 0000000000..b77232d81d --- /dev/null +++ b/dom/security/test/sri/script.js^headers^ @@ -0,0 +1 @@ +Cache-control: public diff --git a/dom/security/test/sri/script_301.js b/dom/security/test/sri/script_301.js new file mode 100644 index 0000000000..9a95de77cf --- /dev/null +++ b/dom/security/test/sri/script_301.js @@ -0,0 +1 @@ +var load=false; diff --git a/dom/security/test/sri/script_301.js^headers^ b/dom/security/test/sri/script_301.js^headers^ new file mode 100644 index 0000000000..efbfb73346 --- /dev/null +++ b/dom/security/test/sri/script_301.js^headers^ @@ -0,0 +1,2 @@ +HTTP 301 Moved Permanently +Location: http://example.com/tests/dom/security/test/sri/script_crossdomain5.js diff --git a/dom/security/test/sri/script_302.js b/dom/security/test/sri/script_302.js new file mode 100644 index 0000000000..9a95de77cf --- /dev/null +++ b/dom/security/test/sri/script_302.js @@ -0,0 +1 @@ +var load=false; diff --git a/dom/security/test/sri/script_302.js^headers^ b/dom/security/test/sri/script_302.js^headers^ new file mode 100644 index 0000000000..05a545a6a1 --- /dev/null +++ b/dom/security/test/sri/script_302.js^headers^ @@ -0,0 +1,2 @@ +HTTP 302 Found +Location: /tests/dom/security/test/sri/script.js diff --git a/dom/security/test/sri/script_401.js b/dom/security/test/sri/script_401.js new file mode 100644 index 0000000000..8fd8f96b2f --- /dev/null +++ b/dom/security/test/sri/script_401.js @@ -0,0 +1 @@ +var load=true; diff --git a/dom/security/test/sri/script_401.js^headers^ b/dom/security/test/sri/script_401.js^headers^ new file mode 100644 index 0000000000..889fbe081a --- /dev/null +++ b/dom/security/test/sri/script_401.js^headers^ @@ -0,0 +1,2 @@ +HTTP 401 Authorization Required +Cache-control: public diff --git a/dom/security/test/sri/script_crossdomain1.js b/dom/security/test/sri/script_crossdomain1.js new file mode 100644 index 0000000000..1f17a6db24 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain1.js @@ -0,0 +1,4 @@ +/* + * this file should be loaded, because it has CORS enabled. +*/ +window.hasCORSLoaded = true; diff --git a/dom/security/test/sri/script_crossdomain1.js^headers^ b/dom/security/test/sri/script_crossdomain1.js^headers^ new file mode 100644 index 0000000000..3a6a85d894 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain1.js^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: http://mochi.test:8888 diff --git a/dom/security/test/sri/script_crossdomain2.js b/dom/security/test/sri/script_crossdomain2.js new file mode 100644 index 0000000000..4b0208ab34 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain2.js @@ -0,0 +1,5 @@ +/* + * this file should not be loaded, because it does not have CORS + * enabled. + */ +window.hasNonCORSLoaded = true; diff --git a/dom/security/test/sri/script_crossdomain3.js b/dom/security/test/sri/script_crossdomain3.js new file mode 100644 index 0000000000..eed05d59b7 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain3.js @@ -0,0 +1 @@ +// This script intentionally left blank diff --git a/dom/security/test/sri/script_crossdomain3.js^headers^ b/dom/security/test/sri/script_crossdomain3.js^headers^ new file mode 100644 index 0000000000..3a6a85d894 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain3.js^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: http://mochi.test:8888 diff --git a/dom/security/test/sri/script_crossdomain4.js b/dom/security/test/sri/script_crossdomain4.js new file mode 100644 index 0000000000..eed05d59b7 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain4.js @@ -0,0 +1 @@ +// This script intentionally left blank diff --git a/dom/security/test/sri/script_crossdomain4.js^headers^ b/dom/security/test/sri/script_crossdomain4.js^headers^ new file mode 100644 index 0000000000..3a6a85d894 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain4.js^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: http://mochi.test:8888 diff --git a/dom/security/test/sri/script_crossdomain5.js b/dom/security/test/sri/script_crossdomain5.js new file mode 100644 index 0000000000..eed05d59b7 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain5.js @@ -0,0 +1 @@ +// This script intentionally left blank diff --git a/dom/security/test/sri/script_crossdomain5.js^headers^ b/dom/security/test/sri/script_crossdomain5.js^headers^ new file mode 100644 index 0000000000..cb762eff80 --- /dev/null +++ b/dom/security/test/sri/script_crossdomain5.js^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: * diff --git a/dom/security/test/sri/style1.css b/dom/security/test/sri/style1.css new file mode 100644 index 0000000000..c7ab9ecffa --- /dev/null +++ b/dom/security/test/sri/style1.css @@ -0,0 +1,3 @@ +#red-text { + color: red; +} diff --git a/dom/security/test/sri/style1.css^headers^ b/dom/security/test/sri/style1.css^headers^ new file mode 100644 index 0000000000..3a6a85d894 --- /dev/null +++ b/dom/security/test/sri/style1.css^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: http://mochi.test:8888 diff --git a/dom/security/test/sri/style2.css b/dom/security/test/sri/style2.css new file mode 100644 index 0000000000..9eece75e5b --- /dev/null +++ b/dom/security/test/sri/style2.css @@ -0,0 +1 @@ +; A valid but somewhat uninteresting stylesheet diff --git a/dom/security/test/sri/style3.css b/dom/security/test/sri/style3.css new file mode 100644 index 0000000000..b64fa3b749 --- /dev/null +++ b/dom/security/test/sri/style3.css @@ -0,0 +1,3 @@ +#black-text { + color: green; +} diff --git a/dom/security/test/sri/style4.css b/dom/security/test/sri/style4.css new file mode 100644 index 0000000000..eab83656ed --- /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 0000000000..e13897f157 --- /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 0000000000..5d59134cc6 --- /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 0000000000..569557694d --- /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 0000000000..d866aa5224 --- /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 0000000000..c7ab9ecffa --- /dev/null +++ b/dom/security/test/sri/style_301.css @@ -0,0 +1,3 @@ +#red-text { + color: red; +} diff --git a/dom/security/test/sri/style_301.css^headers^ b/dom/security/test/sri/style_301.css^headers^ new file mode 100644 index 0000000000..c5b78ee04b --- /dev/null +++ b/dom/security/test/sri/style_301.css^headers^ @@ -0,0 +1,2 @@ +HTTP 301 Moved Permanently +Location: http://example.com/tests/dom/security/test/sri/style1.css diff --git a/dom/security/test/sri/style_imported.css b/dom/security/test/sri/style_imported.css new file mode 100644 index 0000000000..4469e8d78d --- /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 0000000000..985dd34678 --- /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 0000000000..9c74cc64ea --- /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 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_bug_1364262.html b/dom/security/test/sri/test_bug_1364262.html new file mode 100644 index 0000000000..cf77c7dac1 --- /dev/null +++ b/dom/security/test/sri/test_bug_1364262.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + SimpleTest.setExpected(["pass", 1]); + + function good_correctlyBlockedStylesheet() { + ok(true, "Non-base64 hash blocked the load.") + }; + function bad_shouldNotLoadStylesheet() { + ok(false, "Non-base64 hashes should not load!") + } + window.onload = function() { + SimpleTest.finish(); + } + + let link = document.createElement('link'); + document.head.appendChild(link); + link.setAttribute('rel', 'stylesheet'); + link.onerror = good_correctlyBlockedStylesheet; + link.onload = bad_shouldNotLoadStylesheet; + link.integrity = 'sha512-\uD89D\uDF05\uD89D\uDEE6'; + link.setAttribute('href', 'data:text/css;small[contenteditable^="false"], summary { }'); + </script> +</head> +<body> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1364262">Bug 1364262</a> +</body> +</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 0000000000..2f9b27bfa4 --- /dev/null +++ b/dom/security/test/sri/test_script_crossdomain.html @@ -0,0 +1,15 @@ +<!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> +</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 0000000000..d975132a2e --- /dev/null +++ b/dom/security/test/sri/test_script_sameorigin.html @@ -0,0 +1,15 @@ +<!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> +</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_style_crossdomain.html b/dom/security/test/sri/test_style_crossdomain.html new file mode 100644 index 0000000000..eb4dac4cc4 --- /dev/null +++ b/dom/security/test/sri/test_style_crossdomain.html @@ -0,0 +1,15 @@ +<!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> +</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 0000000000..9b85eaf71b --- /dev/null +++ b/dom/security/test/sri/test_style_sameorigin.html @@ -0,0 +1,15 @@ +<!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> +</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 0000000000..3df7a001d5 --- /dev/null +++ b/dom/security/test/unit/test_csp_reports.js @@ -0,0 +1,315 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { HttpServer } = ChromeUtils.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"; + +/** + * 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) { + Assert.equal(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-sample'; " + + "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"); + + principal = Services.scriptSecurityManager.createContentPrincipal( + selfuri, + {} + ); + csp.setRequestContextWithPrincipal(principal, selfuri, "", 0); + + // 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() { + do_get_profile(); + + var selfuri = NetUtil.newURI( + REPORT_SERVER_URI + ":" + REPORT_SERVER_PORT + "/foo/self" + ); + + // test that inline script violations cause a report. + makeTest( + 0, + { + "blocked-uri": "inline", + "effective-directive": "script-src-elem", + disposition: "enforce", + }, + false, + function(csp) { + let inlineOK = true; + inlineOK = csp.getAllowsInline( + Ci.nsIContentSecurityPolicy.SCRIPT_SRC_ELEM_DIRECTIVE, + false, // aHasUnsafeHash + "", // aNonce + false, // aParserCreated + null, // aTriggeringElement + null, // nsICSPEventListener + "", // aContentOfPseudoScript + 0, // aLineNumber + 0 + ); // aColumnNumber + + // this is not a report only policy, so it better block inline scripts + Assert.ok(!inlineOK); + } + ); + + // test that eval violations cause a report. + makeTest( + 1, + { + "blocked-uri": "eval", + // JSON script-sample is UTF8 encoded + "script-sample": "\xc2\xa3\xc2\xa5\xc2\xb5\xe5\x8c\x97\xf0\xa0\x9d\xb9", + "line-number": 1, + "column-number": 2, + }, + 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 + Assert.ok(!evalOK); + // ... and cause reports to go out + Assert.ok(oReportViolation.value); + + if (oReportViolation.value) { + // force the logging, since the getter doesn't. + csp.logViolationDetails( + Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_EVAL, + null, // aTriggeringElement + null, // nsICSPEventListener + 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, // line number + 2 + ); // column number + } + } + ); + + makeTest(2, { "blocked-uri": "http://blocked.test/foo.js" }, false, function( + csp + ) { + // shouldLoad creates and sends out the report here. + csp.shouldLoad( + Ci.nsIContentPolicy.TYPE_SCRIPT, + null, // nsICSPEventListener + NetUtil.newURI("http://blocked.test/foo.js"), + null, + true, + null, + false + ); + }); + + // test that inline script violations cause a report in report-only policy + makeTest( + 3, + { "blocked-uri": "inline", disposition: "report" }, + true, + function(csp) { + let inlineOK = true; + inlineOK = csp.getAllowsInline( + Ci.nsIContentSecurityPolicy.SCRIPT_SRC_ELEM_DIRECTIVE, + false, // aHasUnsafeHash + "", // aNonce + false, // aParserCreated + null, // aTriggeringElement + null, // nsICSPEventListener + "", // aContentOfPseudoScript + 0, // aLineNumber + 0 + ); // aColumnNumber + + // this is a report only policy, so it better allow inline scripts + Assert.ok(inlineOK); + } + ); + + // test that eval violations cause a report in report-only policy + makeTest(4, { "blocked-uri": "eval" }, true, function(csp) { + let evalOK = true, + oReportViolation = { value: false }; + evalOK = csp.getAllowsEval(oReportViolation); + + // this is a report only policy, so it better allow eval + Assert.ok(evalOK); + // ... but still cause reports to go out + Assert.ok(oReportViolation.value); + + if (oReportViolation.value) { + // force the logging, since the getter doesn't. + csp.logViolationDetails( + Ci.nsIContentSecurityPolicy.VIOLATION_TYPE_EVAL, + null, // aTriggeringElement + null, // nsICSPEventListener + selfuri.asciiSpec, + "script sample", + 4, // line number + 5 + ); // column number + } + }); + + // 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, + null, // nsICSPEventListener + NetUtil.newURI("data:image/png;base64," + base64data), + null, + true, + null, + false + ); + }); + + // 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, + null, // nsICSPEventListener + NetUtil.newURI("intent://mymaps.com/maps?um=1&ie=UTF-8&fb=1&sll"), + null, + true, + null, + false + ); + }); + + // test fragment removal + var selfSpec = + REPORT_SERVER_URI + ":" + REPORT_SERVER_PORT + "/foo/self/foo.js"; + makeTest(7, { "blocked-uri": selfSpec }, false, function(csp) { + // shouldLoad creates and sends out the report here. + csp.shouldLoad( + Ci.nsIContentPolicy.TYPE_SCRIPT, + null, // nsICSPEventListener + NetUtil.newURI(selfSpec + "#bar"), + null, + true, + null, + false + ); + }); + + // test scheme of ftp: + makeTest( + 8, + { "blocked-uri": "ftp://blocked.test/profile.png" }, + false, + function(csp) { + // shouldLoad creates and sends out the report here. + csp.shouldLoad( + Ci.nsIContentPolicy.TYPE_SCRIPT, + null, // nsICSPEventListener + NetUtil.newURI("ftp://blocked.test/profile.png"), + null, + true, + null, + false + ); + } + ); +} 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 0000000000..287dc179c5 --- /dev/null +++ b/dom/security/test/unit/test_csp_upgrade_insecure_request_header.js @@ -0,0 +1,102 @@ +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +// 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. +Services.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_IMAGE", + expectingHeader: false, + contentType: Ci.nsIContentPolicy.TYPE_IMAGE, + }, +]; + +function ChannelListener() {} + +ChannelListener.prototype = { + onStartRequest(request) {}, + onDataAvailable(request, stream, offset, count) { + do_throw("Should not get any data!"); + }, + onStopRequest(request, 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); + Assert.equal(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.asyncOpen(new ChannelListener()); +} + +function run_test() { + do_get_profile(); + + // 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_deserialization_format_before_100.js b/dom/security/test/unit/test_deserialization_format_before_100.js new file mode 100644 index 0000000000..a21ae4838a --- /dev/null +++ b/dom/security/test/unit/test_deserialization_format_before_100.js @@ -0,0 +1,244 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +async function runTest(setupFunc, expected) { + let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIObjectOutputStream + ); + let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init( + false /* non-blocking input */, + false /* non-blocking output */, + 0 /* segment size */, + 0 /* max segments */ + ); + objectOutStream.setOutputStream(pipe.outputStream); + + setupFunc(objectOutStream); + + let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIObjectInputStream + ); + objectInStream.setInputStream(pipe.inputStream); + + let referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY); + try { + referrerInfo.read(objectInStream); + } catch (e) { + Assert.ok(false, "Shouldn't fail when deserializing."); + return; + } + + Assert.ok(true, "Successfully deserialize the referrerInfo."); + + let { referrerPolicy, sendReferrer, computedReferrerSpec } = expected; + Assert.equal( + referrerInfo.referrerPolicy, + referrerPolicy, + "The referrerInfo has the expected referrer policy." + ); + + Assert.equal( + referrerInfo.sendReferrer, + sendReferrer, + "The referrerInfo has the expected sendReferrer value." + ); + + if (computedReferrerSpec) { + Assert.equal( + referrerInfo.computedReferrerSpec, + computedReferrerSpec, + "The referrerInfo has the expected computedReferrerSpec value." + ); + } +} + +// Test deserializing referrer info with the old format. +add_task(async function test_deserializeOldReferrerInfo() { + // Test with a regular old format. + await runTest( + stream => { + // Write to the output stream with the old format. + stream.writeBoolean(true); // nonNull + stream.writeStringZ("https://example.com/"); // spec + stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy + stream.writeBoolean(false); // sendReferrer + stream.writeBoolean(false); // isComputed + stream.writeBoolean(true); // initialized + stream.writeBoolean(false); // overridePolicyByDefault + }, + { + referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + sendReferrer: false, + } + ); + + // Test with an old format with `sendReferrer` is true. + await runTest( + stream => { + // Write to the output stream with the old format. + stream.writeBoolean(true); // nonNull + stream.writeStringZ("https://example.com/"); // spec + stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy + stream.writeBoolean(true); // sendReferrer + stream.writeBoolean(false); // isComputed + stream.writeBoolean(true); // initialized + stream.writeBoolean(false); // overridePolicyByDefault + }, + { + referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + sendReferrer: true, + } + ); + + // Test with an old format with a computed Referrer. + await runTest( + stream => { + // Write to the output stream with the old format with a string. + stream.writeBoolean(true); // nonNull + stream.writeStringZ("https://example.com/"); // spec + stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy + stream.writeBoolean(false); // sendReferrer + stream.writeBoolean(true); // isComputed + stream.writeStringZ("https://example.com/"); // computedReferrer + stream.writeBoolean(true); // initialized + stream.writeBoolean(false); // overridePolicyByDefault + }, + { + referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + sendReferrer: false, + computedReferrerSpec: "https://example.com/", + } + ); + + // Test with an old format with a computed Referrer and sendReferrer as true. + await runTest( + stream => { + // Write to the output stream with the old format with a string. + stream.writeBoolean(true); // nonNull + stream.writeStringZ("https://example.com/"); // spec + stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy + stream.writeBoolean(true); // sendReferrer + stream.writeBoolean(true); // isComputed + stream.writeStringZ("https://example.com/"); // computedReferrer + stream.writeBoolean(true); // initialized + stream.writeBoolean(false); // overridePolicyByDefault + }, + { + referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + sendReferrer: true, + computedReferrerSpec: "https://example.com/", + } + ); +}); + +// Test deserializing referrer info with the current format. +add_task(async function test_deserializeReferrerInfo() { + // Test with a current format. + await runTest( + stream => { + // Write to the output stream with the new format. + stream.writeBoolean(true); // nonNull + stream.writeStringZ("https://example.com/"); // spec + stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy + stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // original policy + stream.writeBoolean(false); // sendReferrer + stream.writeBoolean(false); // isComputed + stream.writeBoolean(true); // initialized + stream.writeBoolean(false); // overridePolicyByDefault + }, + { + referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + sendReferrer: false, + } + ); + + // Test with a current format with sendReferrer as true. + await runTest( + stream => { + // Write to the output stream with the new format. + stream.writeBoolean(true); // nonNull + stream.writeStringZ("https://example.com/"); // spec + stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy + stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // original policy + stream.writeBoolean(true); // sendReferrer + stream.writeBoolean(false); // isComputed + stream.writeBoolean(true); // initialized + stream.writeBoolean(false); // overridePolicyByDefault + }, + { + referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + sendReferrer: true, + } + ); + + // Test with a current format with a computedReferrer. + await runTest( + stream => { + // Write to the output stream with the new format with a string. + stream.writeBoolean(true); // nonNull + stream.writeStringZ("https://example.com/"); // spec + stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy + stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // original policy + stream.writeBoolean(false); // sendReferrer + stream.writeBoolean(true); // isComputed + stream.writeStringZ("https://example.com/"); // computedReferrer + stream.writeBoolean(true); // initialized + stream.writeBoolean(false); // overridePolicyByDefault + }, + { + referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + sendReferrer: false, + computedReferrerSpec: "https://example.com/", + } + ); + + // Test with a current format with a computedReferrer and sendReferrer as true. + await runTest( + stream => { + // Write to the output stream with the new format with a string. + stream.writeBoolean(true); // nonNull + stream.writeStringZ("https://example.com/"); // spec + stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // policy + stream.write32(Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN); // original policy + stream.writeBoolean(true); // sendReferrer + stream.writeBoolean(true); // isComputed + stream.writeStringZ("https://example.com/"); // computedReferrer + stream.writeBoolean(true); // initialized + stream.writeBoolean(false); // overridePolicyByDefault + }, + { + referrerPolicy: Ci.nsIReferrerInfo.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + sendReferrer: true, + computedReferrerSpec: "https://example.com/", + } + ); + + // Test with a current format that the tailing bytes are all zero. + await runTest( + stream => { + // Write to the output stream with the new format with a string. + stream.writeBoolean(true); // nonNull + stream.writeStringZ("https://example.com/"); // spec + stream.write32(Ci.nsIReferrerInfo.EMPTY); // policy + stream.write32(Ci.nsIReferrerInfo.EMPTY); // original policy + stream.writeBoolean(false); // sendReferrer + stream.writeBoolean(false); // isComputed + stream.writeBoolean(false); // initialized + stream.writeBoolean(false); // overridePolicyByDefault + }, + { + referrerPolicy: Ci.nsIReferrerInfo.EMPTY, + sendReferrer: false, + } + ); +}); diff --git a/dom/security/test/unit/test_https_only_https_first_default_port.js b/dom/security/test/unit/test_https_only_https_first_default_port.js new file mode 100644 index 0000000000..8960fd74b0 --- /dev/null +++ b/dom/security/test/unit/test_https_only_https_first_default_port.js @@ -0,0 +1,107 @@ +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +const TEST_PATH = "/https_only_https_first_port"; +var httpserver = null; +var channel = null; +var curTest = null; + +const TESTS = [ + { + description: "Test 1 - Default Port (scheme: http, port: default)", + url: "http://test1.example.com", + expectedScheme: "https", + expectedPort: -1, // -1 == default + }, + { + description: "Test 2 - Explicit Default Port (scheme: http, port: 80)", + url: "http://test1.example.com:80", + expectedScheme: "https", + expectedPort: -1, // -1 == default + }, + { + description: "Test 3 - Explicit Custom Port (scheme: http, port: 8888)", + url: "http://test1.example.com:8888", + expectedScheme: "http", + expectedPort: 8888, + }, + { + description: + "Test 4 - Explicit Default Port for https (scheme: https, port: 443)", + url: "https://test1.example.com:443", + expectedScheme: "https", + expectedPort: -1, // -1 == default + }, +]; + +function ChannelListener() {} + +ChannelListener.prototype = { + onStartRequest(request) { + // dummy implementation + }, + onDataAvailable(request, stream, offset, count) { + do_throw("Should not get any data!"); + }, + onStopRequest(request, status) { + var chan = request.QueryInterface(Ci.nsIChannel); + let requestURL = chan.URI; + Assert.equal( + requestURL.scheme, + curTest.expectedScheme, + curTest.description + ); + Assert.equal(requestURL.port, curTest.expectedPort, curTest.description); + Assert.equal(requestURL.host, "test1.example.com", curTest.description); + run_next_test(); + }, +}; + +function setUpPrefs() { + // set up the required prefs + Services.prefs.setBoolPref("dom.security.https_first", true); + Services.prefs.setBoolPref("dom.security.https_only_mode", false); +} + +function setUpChannel() { + var chan = NetUtil.newChannel({ + uri: curTest.url + TEST_PATH, + loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + }); + chan.QueryInterface(Ci.nsIHttpChannel); + chan.requestMethod = "GET"; + return chan; +} + +function serverHandler(metadata, response) { + // dummy implementation +} + +function run_next_test() { + curTest = TESTS.shift(); + if (!curTest) { + httpserver.stop(do_test_finished); + return; + } + + channel = setUpChannel(); + channel.asyncOpen(new ChannelListener()); +} + +function run_test() { + do_get_profile(); + do_test_pending(); + + // set up the test environment + httpserver = new HttpServer(); + httpserver.registerPathHandler(TEST_PATH, serverHandler); + httpserver.start(-1); + + // set up prefs + setUpPrefs(); + + // run the tests + run_next_test(); +} diff --git a/dom/security/test/unit/test_https_only_https_first_prefs.js b/dom/security/test/unit/test_https_only_https_first_prefs.js new file mode 100644 index 0000000000..0b2f0cd1dd --- /dev/null +++ b/dom/security/test/unit/test_https_only_https_first_prefs.js @@ -0,0 +1,356 @@ +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +XPCOMUtils.defineLazyGetter(this, "HTTP_TEST_URL", function() { + return "http://test1.example.com"; +}); + +const TEST_PATH = "/https_only_https_first_path"; +var httpserver = null; +var channel = null; +var curTest = null; + +const TESTS = [ + { + // Test 1: all prefs to false + description: "Test 1 - top-level", + contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + https_only: false, + https_only_pbm: false, + https_first: false, + https_first_pbm: false, + pbm: false, + expectedScheme: "http", + }, + { + description: "Test 1 - top-level - pbm", + contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + https_only: false, + https_only_pbm: false, + https_first: false, + https_first_pbm: false, + pbm: true, + expectedScheme: "http", + }, + { + description: "Test 1 - sub-resource", + contentType: Ci.nsIContentPolicy.TYPE_IMAGE, + https_only: false, + https_only_pbm: false, + https_first: false, + https_first_pbm: false, + pbm: false, + expectedScheme: "http", + }, + { + description: "Test 1 - sub-resource - pbm", + contentType: Ci.nsIContentPolicy.TYPE_IMAGE, + https_only: false, + https_only_pbm: false, + https_first: false, + https_first_pbm: false, + pbm: true, + expectedScheme: "http", + }, + // Test 2: https_only true + { + description: "Test 2 - top-level", + contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + https_only: true, + https_only_pbm: false, + https_first: false, + https_first_pbm: false, + pbm: false, + expectedScheme: "https", + }, + { + description: "Test 2 - top-level - pbm", + contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + https_only: true, + https_only_pbm: false, + https_first: false, + https_first_pbm: false, + pbm: true, + expectedScheme: "https", + }, + { + description: "Test 2 - sub-resource", + contentType: Ci.nsIContentPolicy.TYPE_IMAGE, + https_only: true, + https_only_pbm: false, + https_first: false, + https_first_pbm: false, + pbm: false, + expectedScheme: "https", + }, + { + description: "Test 2 - sub-resource - pbm", + contentType: Ci.nsIContentPolicy.TYPE_IMAGE, + https_only: true, + https_only_pbm: false, + https_first: false, + https_first_pbm: false, + pbm: true, + expectedScheme: "https", + }, + // Test 3: https_only_pbm true + { + description: "Test 3 - top-level", + contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + https_only: false, + https_only_pbm: true, + https_first: false, + https_first_pbm: false, + pbm: false, + expectedScheme: "http", + }, + { + description: "Test 3 - top-level - pbm", + contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + https_only: false, + https_only_pbm: true, + https_first: false, + https_first_pbm: false, + pbm: true, + expectedScheme: "https", + }, + { + description: "Test 3 - sub-resource", + contentType: Ci.nsIContentPolicy.TYPE_IMAGE, + https_only: false, + https_only_pbm: true, + https_first: false, + https_first_pbm: false, + pbm: false, + expectedScheme: "http", + }, + { + description: "Test 3 - sub-resource - pbm", + contentType: Ci.nsIContentPolicy.TYPE_IMAGE, + https_only: false, + https_only_pbm: true, + https_first: false, + https_first_pbm: false, + pbm: true, + expectedScheme: "https", + }, + // Test 4: https_first true + { + description: "Test 4 - top-level", + contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + https_only: false, + https_only_pbm: false, + https_first: true, + https_first_pbm: false, + pbm: false, + expectedScheme: "https", + }, + { + description: "Test 4 - top-level - pbm", + contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + https_only: false, + https_only_pbm: false, + https_first: true, + https_first_pbm: false, + pbm: true, + expectedScheme: "https", + }, + { + description: "Test 4 - sub-resource", + contentType: Ci.nsIContentPolicy.TYPE_IMAGE, + https_only: false, + https_only_pbm: false, + https_first: true, + https_first_pbm: false, + pbm: false, + expectedScheme: "http", + }, + { + description: "Test 4 - sub-resource - pbm", + contentType: Ci.nsIContentPolicy.TYPE_IMAGE, + https_only: false, + https_only_pbm: false, + https_first: true, + https_first_pbm: false, + pbm: true, + expectedScheme: "http", + }, + // Test 5: https_first_pbm true + { + description: "Test 5 - top-level", + contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + https_only: false, + https_only_pbm: false, + https_first: false, + https_first_pbm: true, + pbm: false, + expectedScheme: "http", + }, + { + description: "Test 5 - top-level - pbm", + contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + https_only: false, + https_only_pbm: false, + https_first: false, + https_first_pbm: true, + pbm: true, + expectedScheme: "https", + }, + { + description: "Test 5 - sub-resource", + contentType: Ci.nsIContentPolicy.TYPE_IMAGE, + https_only: false, + https_only_pbm: false, + https_first: false, + https_first_pbm: true, + pbm: false, + expectedScheme: "http", + }, + { + description: "Test 5 - sub-resource - pbm", + contentType: Ci.nsIContentPolicy.TYPE_IMAGE, + https_only: false, + https_only_pbm: false, + https_first: false, + https_first_pbm: true, + pbm: true, + expectedScheme: "http", + }, + // Test 6: https_only overrules https_first + { + description: "Test 6 - top-level", + contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + https_only: true, + https_only_pbm: false, + https_first: true, + https_first_pbm: false, + pbm: false, + expectedScheme: "https", + }, + { + description: "Test 6 - top-level - pbm", + contentType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + https_only: true, + https_only_pbm: false, + https_first: true, + https_first_pbm: false, + pbm: true, + expectedScheme: "https", + }, + { + description: "Test 6 - sub-resource", + contentType: Ci.nsIContentPolicy.TYPE_IMAGE, + https_only: true, + https_only_pbm: false, + https_first: true, + https_first_pbm: false, + pbm: false, + expectedScheme: "https", + }, + { + description: "Test 6 - sub-resource - pbm", + contentType: Ci.nsIContentPolicy.TYPE_IMAGE, + https_only: true, + https_only_pbm: false, + https_first: true, + https_first_pbm: false, + pbm: true, + expectedScheme: "https", + }, +]; + +function ChannelListener() {} + +ChannelListener.prototype = { + onStartRequest(request) { + // dummy implementation + }, + onDataAvailable(request, stream, offset, count) { + do_throw("Should not get any data!"); + }, + onStopRequest(request, status) { + var chan = request.QueryInterface(Ci.nsIChannel); + let requestURL = chan.URI; + Assert.equal( + requestURL.scheme, + curTest.expectedScheme, + curTest.description + ); + Assert.equal(requestURL.host, "test1.example.com", curTest.description); + Assert.equal(requestURL.filePath, TEST_PATH, curTest.description); + run_next_test(); + }, +}; + +function setUpPrefs() { + // set up the required prefs + Services.prefs.setBoolPref( + "dom.security.https_only_mode", + curTest.https_only + ); + Services.prefs.setBoolPref( + "dom.security.https_only_mode_pbm", + curTest.https_only_pbm + ); + Services.prefs.setBoolPref("dom.security.https_first", curTest.https_first); + Services.prefs.setBoolPref( + "dom.security.https_first_pbm", + curTest.https_first_pbm + ); +} + +function setUpChannel() { + // 1) Set up Principal using OA in case of Private Browsing + let attr = {}; + if (curTest.pbm) { + attr.privateBrowsingId = 1; + } + let uri = Services.io.newURI("http://test1.example.com"); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + attr + ); + + // 2) Set up Channel + var chan = NetUtil.newChannel({ + uri: HTTP_TEST_URL + TEST_PATH, + loadingPrincipal: principal, + contentPolicyType: curTest.contentType, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + }); + chan.QueryInterface(Ci.nsIHttpChannel); + chan.requestMethod = "GET"; + return chan; +} + +function serverHandler(metadata, response) { + // dummy implementation +} + +function run_next_test() { + curTest = TESTS.shift(); + if (!curTest) { + httpserver.stop(do_test_finished); + return; + } + + setUpPrefs(); + + channel = setUpChannel(); + channel.asyncOpen(new ChannelListener()); +} + +function run_test() { + do_get_profile(); + + // set up the test environment + httpserver = new HttpServer(); + httpserver.registerPathHandler(TEST_PATH, 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 0000000000..b218f1438f --- /dev/null +++ b/dom/security/test/unit/test_isOriginPotentiallyTrustworthy.js @@ -0,0 +1,51 @@ +/* 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 { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +Services.prefs.setCharPref( + "dom.securecontext.allowlist", + "example.net,example.org" +); + +Services.prefs.setBoolPref("dom.securecontext.allowlist_onions", false); + +add_task(async function test_isOriginPotentiallyTrustworthy() { + for (let [uriSpec, expectedResult] of [ + ["http://example.com/", false], + ["https://example.com/", true], + ["http://localhost/", true], + ["http://localhost.localhost/", true], + ["http://127.0.0.1/", true], + ["file:///", true], + ["resource:///", true], + ["moz-extension://", true], + ["wss://example.com/", true], + ["about:config", false], + ["http://example.net/", true], + ["ws://example.org/", true], + ["chrome://example.net/content/messenger.xul", false], + ["http://1234567890abcdef.onion/", false], + ]) { + let uri = NetUtil.newURI(uriSpec); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + Assert.equal(principal.isOriginPotentiallyTrustworthy, expectedResult); + } + // And now let's test whether .onion sites are properly treated when + // allowlisted, see bug 1382359. + Services.prefs.setBoolPref("dom.securecontext.allowlist_onions", true); + let uri = NetUtil.newURI("http://1234567890abcdef.onion/"); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + Assert.equal(principal.isOriginPotentiallyTrustworthy, true); +}); diff --git a/dom/security/test/unit/xpcshell.ini b/dom/security/test/unit/xpcshell.ini new file mode 100644 index 0000000000..b5da5879e6 --- /dev/null +++ b/dom/security/test/unit/xpcshell.ini @@ -0,0 +1,11 @@ +[DEFAULT] +head = + +[test_csp_reports.js] +[test_isOriginPotentiallyTrustworthy.js] +[test_deserialization_format_before_100.js] +[test_csp_upgrade_insecure_request_header.js] +[test_https_only_https_first_prefs.js] +skip-if = debug # assertions in loadinfo; loadinfo is just not xpcshell test friendly +[test_https_only_https_first_default_port.js] +skip-if = debug # assertions in loadinfo; loadinfo is just not xpcshell test friendly |