TestUnexpectedPrivilegedLoads.cpp (13216B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include <string.h> 8 9 #include "TelemetryFixture.h" 10 #include "TelemetryTestHelpers.h" 11 #include "core/TelemetryEvent.h" 12 #include "gtest/gtest.h" 13 #include "js/Array.h" // JS::GetArrayLength 14 #include "js/PropertyAndElement.h" // JS_GetElement, JS_GetProperty 15 #include "js/TypeDecls.h" 16 #include "mozilla/BasePrincipal.h" 17 #include "mozilla/Preferences.h" 18 #include "mozilla/RefPtr.h" 19 #include "mozilla/Telemetry.h" 20 #include "nsContentSecurityManager.h" 21 #include "nsContentSecurityUtils.h" 22 #include "nsContentUtils.h" 23 #include "nsIContentPolicy.h" 24 #include "nsILoadInfo.h" 25 #include "nsNetUtil.h" 26 #include "nsStringFwd.h" 27 28 using namespace mozilla; 29 using namespace TelemetryTestHelpers; 30 31 extern Atomic<bool, mozilla::Relaxed> sJSHacksChecked; 32 extern Atomic<bool, mozilla::Relaxed> sJSHacksPresent; 33 extern Atomic<bool, mozilla::Relaxed> sCSSHacksChecked; 34 extern Atomic<bool, mozilla::Relaxed> sCSSHacksPresent; 35 36 TEST_F(TelemetryTestFixture, UnexpectedPrivilegedLoadsTelemetryTest) { 37 // Enable telemetry pref. 38 bool prefDefault = Preferences::GetBool( 39 "dom.security.unexpected_system_load_telemetry_enabled"); 40 Preferences::SetBool("dom.security.unexpected_system_load_telemetry_enabled", 41 true); 42 43 // Disable JS/CSS Hacks Detection, which would consider this current profile 44 // as uninteresting for our measurements: 45 bool origJSHacksPresent = sJSHacksPresent; 46 bool origJSHacksChecked = sJSHacksChecked; 47 sJSHacksPresent = false; 48 sJSHacksChecked = true; 49 bool origCSSHacksPresent = sCSSHacksPresent; 50 bool origCSSHacksChecked = sCSSHacksChecked; 51 sCSSHacksPresent = false; 52 sCSSHacksChecked = true; 53 54 struct testResults { 55 nsCString fileinfo; 56 nsCString extraValueContenttype; 57 nsCString extraValueRemotetype; 58 nsCString extraValueFiledetails; 59 nsCString extraValueRedirects; 60 }; 61 62 struct testCasesAndResults { 63 nsCString urlstring; 64 nsContentPolicyType contentType; 65 nsCString remoteType; 66 testResults expected; 67 }; 68 69 AutoJSContextWithGlobal cx(mCleanGlobal); 70 // Make sure we don't look at events from other tests. 71 (void)mTelemetry->ClearEvents(); 72 73 // required for telemetry lookups 74 constexpr auto category = "security"_ns; 75 constexpr auto method = "unexpectedload"_ns; 76 constexpr auto object = "systemprincipal"_ns; 77 constexpr auto extraKeyContenttype = "contenttype"_ns; 78 constexpr auto extraKeyRemotetype = "remotetype"_ns; 79 constexpr auto extraKeyFiledetails = "filedetails"_ns; 80 constexpr auto extraKeyRedirects = "redirects"_ns; 81 82 // some cases from TestFilenameEvalParser 83 // no need to replicate all scenarios?! 84 testCasesAndResults myTestCases[] = { 85 {"chrome://firegestures/content/browser.js"_ns, 86 nsContentPolicyType::TYPE_SCRIPT, 87 "web"_ns, 88 {"chromeuri"_ns, "TYPE_SCRIPT"_ns, "web"_ns, 89 "chrome://firegestures/content/browser.js"_ns, ""_ns}}, 90 {"resource://firegestures/content/browser.js"_ns, 91 nsContentPolicyType::TYPE_SCRIPT, 92 "web"_ns, 93 {"resourceuri"_ns, "TYPE_SCRIPT"_ns, "web"_ns, 94 "resource://firegestures/content/browser.js"_ns, ""_ns}}, 95 {// test that we don't report blob details 96 // ..and test that we strip of URLs from remoteTypes 97 "blob://000-000"_ns, 98 nsContentPolicyType::TYPE_SCRIPT, 99 "webIsolated=https://blob.example/"_ns, 100 {"bloburi"_ns, "TYPE_SCRIPT"_ns, "webIsolated"_ns, "unknown"_ns, ""_ns}}, 101 {// test for cases where finalURI is null, due to a broken nested URI 102 // .. like malformed moz-icon URLs 103 "moz-icon:blahblah"_ns, 104 nsContentPolicyType::TYPE_DOCUMENT, 105 "web"_ns, 106 {"other"_ns, "TYPE_DOCUMENT"_ns, "web"_ns, "unknown"_ns, ""_ns}}, 107 {// we dont report data urls 108 // ..and test that we strip of URLs from remoteTypes 109 "data://blahblahblah"_ns, 110 nsContentPolicyType::TYPE_SCRIPT, 111 "webCOOP+COEP=https://data.example"_ns, 112 {"dataurl"_ns, "TYPE_SCRIPT"_ns, "webCOOP+COEP"_ns, "unknown"_ns, 113 ""_ns}}, 114 {// handle data URLs for webextension content scripts differently 115 // .. by noticing their annotation 116 "data:text/css;extension=style;charset=utf-8,/* some css here */"_ns, 117 nsContentPolicyType::TYPE_STYLESHEET, 118 "web"_ns, 119 {"dataurl-extension-contentstyle"_ns, "TYPE_STYLESHEET"_ns, "web"_ns, 120 "unknown"_ns, ""_ns}}, 121 {// we only report file URLs on windows, where we can easily sanitize 122 "file://c/users/tom/file.txt"_ns, 123 nsContentPolicyType::TYPE_SCRIPT, 124 "web"_ns, 125 { 126 #if defined(XP_WIN) 127 "sanitizedWindowsURL"_ns, "TYPE_SCRIPT"_ns, "web"_ns, 128 "file://.../file.txt"_ns, ""_ns 129 130 #else 131 "other"_ns, "TYPE_SCRIPT"_ns, "web"_ns, "unknown"_ns, ""_ns 132 #endif 133 }}, 134 {// test for one redirect 135 "moz-extension://abcdefab-1234-4321-0000-abcdefabcdef/js/assets.js"_ns, 136 nsContentPolicyType::TYPE_SCRIPT, 137 "web"_ns, 138 {"extension_uri"_ns, "TYPE_SCRIPT"_ns, "web"_ns, 139 // the extension-id is made-up, so the extension will report failure 140 "moz-extension://[failed finding addon by host]/js/assets.js"_ns, 141 "https"_ns}}, 142 {// test for cases where finalURI is empty 143 ""_ns, 144 nsContentPolicyType::TYPE_STYLESHEET, 145 "web"_ns, 146 {"other"_ns, "TYPE_STYLESHEET"_ns, "web"_ns, "unknown"_ns, ""_ns}}, 147 {// test for cases where finalURI is null, due to the struct layout, we'll 148 // override the URL with nullptr in loop below. 149 "URLWillResultInNullPtr"_ns, 150 nsContentPolicyType::TYPE_SCRIPT, 151 "web"_ns, 152 {"other"_ns, "TYPE_SCRIPT"_ns, "web"_ns, "unknown"_ns, ""_ns}}, 153 }; 154 155 int i = 0; 156 for (auto const& currentTest : myTestCases) { 157 nsresult rv; 158 nsCOMPtr<nsIURI> uri; 159 160 // special-casing for a case where the uri is null 161 if (!currentTest.urlstring.Equals("URLWillResultInNullPtr")) { 162 NS_NewURI(getter_AddRefs(uri), currentTest.urlstring); 163 } 164 165 // We can't create channels for chrome: URLs unless they are in a chrome 166 // registry that maps them into the actual destination URL (usually 167 // file://). It seems that gtest don't have chrome manifest registered, so 168 // we'll use a mockChannel with a mockUri. 169 nsCOMPtr<nsIURI> mockUri; 170 rv = NS_NewURI(getter_AddRefs(mockUri), "http://example.com"_ns); 171 ASSERT_EQ(rv, NS_OK) << "Could not create mockUri"; 172 nsCOMPtr<nsIChannel> mockChannel; 173 nsCOMPtr<nsIIOService> service = do_GetIOService(); 174 if (!service) { 175 ASSERT_TRUE(false) 176 << "Couldn't initialize IOService"; 177 } 178 rv = service->NewChannelFromURI( 179 mockUri, nullptr, nsContentUtils::GetSystemPrincipal(), 180 nsContentUtils::GetSystemPrincipal(), 0, currentTest.contentType, 181 getter_AddRefs(mockChannel)); 182 ASSERT_EQ(rv, NS_OK) << "Could not create a mock channel"; 183 nsCOMPtr<nsILoadInfo> mockLoadInfo = mockChannel->LoadInfo(); 184 185 // We're adding a redirect entry for one specific test 186 if (currentTest.urlstring.EqualsASCII( 187 "moz-extension://abcdefab-1234-4321-0000-abcdefabcdef/js/" 188 "assets.js")) { 189 nsCOMPtr<nsIURI> redirUri; 190 NS_NewURI(getter_AddRefs(redirUri), 191 "https://www.analytics.example/analytics.js"_ns); 192 nsCOMPtr<nsIPrincipal> redirPrincipal = 193 BasePrincipal::CreateContentPrincipal(redirUri, OriginAttributes()); 194 nsCOMPtr<nsIChannel> redirectChannel; 195 (void)service->NewChannelFromURI(redirUri, nullptr, redirPrincipal, 196 nullptr, 0, currentTest.contentType, 197 getter_AddRefs(redirectChannel)); 198 199 mockLoadInfo->AppendRedirectHistoryEntry(redirectChannel, false); 200 } 201 202 // this will record the event 203 nsContentSecurityManager::MeasureUnexpectedPrivilegedLoads( 204 mockLoadInfo, uri, currentTest.remoteType); 205 206 // let's inspect the recorded events 207 208 JS::Rooted<JS::Value> eventsSnapshot(cx.GetJSContext()); 209 GetEventSnapshot(cx.GetJSContext(), &eventsSnapshot); 210 211 ASSERT_TRUE(EventPresent(cx.GetJSContext(), eventsSnapshot, category, 212 method, object)) 213 << "Test event with value and extra must be present."; 214 215 // Convert eventsSnapshot into array/object 216 JSContext* aCx = cx.GetJSContext(); 217 JS::Rooted<JSObject*> arrayObj(aCx, &eventsSnapshot.toObject()); 218 219 JS::Rooted<JS::Value> eventRecord(aCx); 220 ASSERT_TRUE(JS_GetElement(aCx, arrayObj, i++, &eventRecord)) 221 << "Must be able to get record."; // record is already undefined :-/ 222 223 ASSERT_TRUE(!eventRecord.isUndefined()) 224 << "eventRecord should not be undefined"; 225 226 JS::Rooted<JSObject*> recordArray(aCx, &eventRecord.toObject()); 227 uint32_t recordLength; 228 ASSERT_TRUE(JS::GetArrayLength(aCx, recordArray, &recordLength)) 229 << "Event record array must have length."; 230 ASSERT_TRUE(recordLength == 6) 231 << "Event record must have 6 elements."; 232 233 JS::Rooted<JS::Value> str(aCx); 234 nsAutoJSString jsStr; 235 // The fileinfo string is at index 4 236 ASSERT_TRUE(JS_GetElement(aCx, recordArray, 4, &str)) 237 << "Must be able to get value."; 238 ASSERT_TRUE(jsStr.init(aCx, str)) 239 << "Value must be able to be init'd to a jsstring."; 240 241 ASSERT_STREQ(NS_ConvertUTF16toUTF8(jsStr).get(), 242 currentTest.expected.fileinfo.get()) 243 << "Reported fileinfo '" << NS_ConvertUTF16toUTF8(jsStr).get() 244 << " 'equals expected value: " << currentTest.expected.fileinfo.get(); 245 246 // Extra is at index 5 247 JS::Rooted<JS::Value> obj(aCx); 248 ASSERT_TRUE(JS_GetElement(aCx, recordArray, 5, &obj)) 249 << "Must be able to get extra data"; 250 JS::Rooted<JSObject*> extraObj(aCx, &obj.toObject()); 251 // looking at remotetype extra for content type 252 JS::Rooted<JS::Value> extraValC(aCx); 253 ASSERT_TRUE( 254 JS_GetProperty(aCx, extraObj, extraKeyContenttype.get(), &extraValC)) 255 << "Must be able to get the extra key's value for contenttype"; 256 ASSERT_TRUE(jsStr.init(aCx, extraValC)) 257 << "Extra value contenttype must be able to be init'd to a jsstring."; 258 ASSERT_STREQ(NS_ConvertUTF16toUTF8(jsStr).get(), 259 currentTest.expected.extraValueContenttype.get()) 260 << "Reported value for extra contenttype '" 261 << NS_ConvertUTF16toUTF8(jsStr).get() 262 << "' should equals supplied value" 263 << currentTest.expected.extraValueContenttype.get(); 264 // and again for remote type 265 JS::Rooted<JS::Value> extraValP(aCx); 266 ASSERT_TRUE( 267 JS_GetProperty(aCx, extraObj, extraKeyRemotetype.get(), &extraValP)) 268 << "Must be able to get the extra key's value for remotetype"; 269 ASSERT_TRUE(jsStr.init(aCx, extraValP)) 270 << "Extra value remotetype must be able to be init'd to a jsstring."; 271 ASSERT_STREQ(NS_ConvertUTF16toUTF8(jsStr).get(), 272 currentTest.expected.extraValueRemotetype.get()) 273 << "Reported value for extra remotetype '" 274 << NS_ConvertUTF16toUTF8(jsStr).get() 275 << "' should equals supplied value: " 276 << currentTest.expected.extraValueRemotetype.get(); 277 // repeating the same for filedetails extra 278 JS::Rooted<JS::Value> extraValF(aCx); 279 ASSERT_TRUE( 280 JS_GetProperty(aCx, extraObj, extraKeyFiledetails.get(), &extraValF)) 281 << "Must be able to get the extra key's value for filedetails"; 282 ASSERT_TRUE(jsStr.init(aCx, extraValF)) 283 << "Extra value filedetails must be able to be init'd to a jsstring."; 284 ASSERT_STREQ(NS_ConvertUTF16toUTF8(jsStr).get(), 285 currentTest.expected.extraValueFiledetails.get()) 286 << "Reported value for extra filedetails '" 287 << NS_ConvertUTF16toUTF8(jsStr).get() << "'should equals supplied value" 288 << currentTest.expected.extraValueFiledetails.get(); 289 // checking the extraKeyRedirects match 290 JS::Rooted<JS::Value> extraValRedirects(aCx); 291 ASSERT_TRUE(JS_GetProperty(aCx, extraObj, extraKeyRedirects.get(), 292 &extraValRedirects)) 293 << "Must be able to get the extra value for redirects"; 294 ASSERT_TRUE(jsStr.init(aCx, extraValRedirects)) 295 << "Extra value redirects must be able to be init'd to a jsstring"; 296 ASSERT_STREQ(NS_ConvertUTF16toUTF8(jsStr).get(), 297 currentTest.expected.extraValueRedirects.get()) 298 << "Reported value for extra redirect '" 299 << NS_ConvertUTF16toUTF8(jsStr).get() 300 << "' should equals supplied value: " 301 << currentTest.expected.extraValueRedirects.get(); 302 } 303 304 // Re-store JS/CSS hacks detection state 305 sJSHacksPresent = origJSHacksPresent; 306 sJSHacksChecked = origJSHacksChecked; 307 sCSSHacksPresent = origCSSHacksPresent; 308 sCSSHacksChecked = origCSSHacksChecked; 309 310 // Restore telemetry pref 311 Preferences::SetBool("dom.security.unexpected_system_load_telemetry_enabled", 312 prefDefault); 313 }