tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }