diff options
Diffstat (limited to 'dom/security/test/gtest/TestUnexpectedPrivilegedLoads.cpp')
-rw-r--r-- | dom/security/test/gtest/TestUnexpectedPrivilegedLoads.cpp | 305 |
1 files changed, 305 insertions, 0 deletions
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; +} |