tor-browser

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

CookieValidation.cpp (17697B)


      1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 #include "CookieValidation.h"
      7 #include "CookieLogging.h"
      8 #include "CookieService.h"
      9 #include "CookiePrefixes.h"
     10 #include "mozilla/dom/nsMixedContentBlocker.h"
     11 #include "mozilla/StaticPrefs_network.h"
     12 
     13 constexpr uint32_t kMaxBytesPerCookie = 4096;
     14 constexpr uint32_t kMaxBytesPerDomain = 1024;
     15 constexpr uint32_t kMaxBytesPerPath = 1024;
     16 
     17 using namespace mozilla::net;
     18 
     19 NS_IMPL_ISUPPORTS(CookieValidation, nsICookieValidation)
     20 
     21 CookieValidation::CookieValidation(const CookieStruct& aCookieData)
     22    : mCookieData(aCookieData) {}
     23 
     24 // static
     25 already_AddRefed<CookieValidation> CookieValidation::Validate(
     26    const CookieStruct& aCookieData) {
     27  RefPtr<CookieValidation> cv = new CookieValidation(aCookieData);
     28  cv->ValidateInternal();
     29  return cv.forget();
     30 }
     31 
     32 // static
     33 already_AddRefed<CookieValidation> CookieValidation::ValidateForHost(
     34    const CookieStruct& aCookieData, nsIURI* aHostURI,
     35    const nsACString& aBaseDomain, bool aRequireHostMatch, bool aFromHttp) {
     36  RefPtr<CookieValidation> cv = new CookieValidation(aCookieData);
     37  cv->ValidateForHostInternal(aHostURI, aBaseDomain, aRequireHostMatch,
     38                              aFromHttp);
     39  return cv.forget();
     40 }
     41 
     42 // static
     43 already_AddRefed<CookieValidation> CookieValidation::ValidateInContext(
     44    const CookieStruct& aCookieData, nsIURI* aHostURI,
     45    const nsACString& aBaseDomain, bool aRequireHostMatch, bool aFromHttp,
     46    bool aIsForeignAndNotAddon, bool aPartitionedOnly,
     47    bool aIsInPrivateBrowsing) {
     48  RefPtr<CookieValidation> cv = new CookieValidation(aCookieData);
     49  cv->ValidateInContextInternal(aHostURI, aBaseDomain, aRequireHostMatch,
     50                                aFromHttp, aIsForeignAndNotAddon,
     51                                aPartitionedOnly, aIsInPrivateBrowsing);
     52  return cv.forget();
     53 }
     54 
     55 void CookieValidation::ValidateInternal() {
     56  MOZ_ASSERT(mResult == eOK);
     57 
     58  // reject cookie if name and value are empty, per RFC6265bis
     59  if (mCookieData.name().IsEmpty() && mCookieData.value().IsEmpty()) {
     60    mResult = eRejectedEmptyNameAndValue;
     61    return;
     62  }
     63 
     64  // reject cookie if it's over the size limit, per RFC2109
     65  if (!CheckNameAndValueSize(mCookieData)) {
     66    mResult = eRejectedNameValueOversize;
     67    return;
     68  }
     69 
     70  if (!CheckName(mCookieData)) {
     71    mResult = eRejectedInvalidCharName;
     72    return;
     73  }
     74 
     75  if (!CheckValue(mCookieData)) {
     76    mResult = eRejectedInvalidCharValue;
     77    return;
     78  }
     79 
     80  if (mCookieData.path().Length() > kMaxBytesPerPath) {
     81    mResult = eRejectedAttributePathOversize;
     82    return;
     83  }
     84 
     85  if (mCookieData.path().Contains('\t')) {
     86    mResult = eRejectedInvalidPath;
     87    return;
     88  }
     89 
     90  if (mCookieData.host().Length() > kMaxBytesPerDomain) {
     91    mResult = eRejectedAttributeDomainOversize;
     92    return;
     93  }
     94 
     95  // If a cookie is nameless, then its value must not start with a known prefix.
     96  if (mCookieData.name().IsEmpty() &&
     97      CookiePrefixes::Has(mCookieData.value())) {
     98    mResult = eRejectedInvalidPrefix;
     99    return;
    100  }
    101 
    102  // If same-site is explicitly set to 'none' but this is not a secure context,
    103  // let's abort the parsing.
    104  if (!mCookieData.isSecure() &&
    105      mCookieData.sameSite() == nsICookie::SAMESITE_NONE) {
    106    if (StaticPrefs::network_cookie_sameSite_noneRequiresSecure()) {
    107      mResult = eRejectedNoneRequiresSecure;
    108      return;
    109    }
    110 
    111    // Still warn about the missing Secure attribute when not enforcing.
    112    mWarnings.mSameSiteNoneRequiresSecureForBeta = true;
    113  }
    114 
    115  // This part checks if the caleers have set the expiry value to max 400 days.
    116  if (!mCookieData.isSession()) {
    117    int64_t maxageCap = StaticPrefs::network_cookie_maxageCap();
    118    int64_t creationTimeInMSec =
    119        mCookieData.updateTimeInUSec() / PR_USEC_PER_MSEC;
    120    int64_t expiryInMSec = mCookieData.expiryInMSec();
    121    if (maxageCap && expiryInMSec > creationTimeInMSec + maxageCap * 1000) {
    122      mResult = eRejectedAttributeExpiryOversize;
    123      return;
    124    }
    125  }
    126 }
    127 
    128 void CookieValidation::ValidateForHostInternal(nsIURI* aHostURI,
    129                                               const nsACString& aBaseDomain,
    130                                               bool aRequireHostMatch,
    131                                               bool aFromHttp) {
    132  MOZ_ASSERT(mResult == eOK);
    133 
    134  ValidateInternal();
    135  if (mResult != eOK) {
    136    return;
    137  }
    138 
    139  if (!aBaseDomain.IsEmpty() &&
    140      !CheckDomain(mCookieData, aHostURI, aBaseDomain, aRequireHostMatch)) {
    141    mResult = eRejectedInvalidDomain;
    142    return;
    143  }
    144 
    145  // if the new cookie is httponly, make sure we're not coming from script
    146  if (!aFromHttp && mCookieData.isHttpOnly()) {
    147    mResult = eRejectedHttpOnlyButFromScript;
    148    return;
    149  }
    150 
    151  bool potentiallyTrustworthy =
    152      nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aHostURI);
    153 
    154  // FixDomain() and FixPath() from CookieParser MUST be run first to make sure
    155  // invalid attributes are rejected and to regularlize them. In particular all
    156  // explicit domain attributes result in a host that starts with a dot, and if
    157  // the host doesn't start with a dot it correctly matches the true
    158  // host.
    159  if (!CookiePrefixes::Check(mCookieData, potentiallyTrustworthy)) {
    160    mResult = eRejectedInvalidPrefix;
    161    return;
    162  }
    163 
    164  // If the new cookie is non-https and wants to set secure flag,
    165  // browser have to ignore this new cookie.
    166  // (draft-ietf-httpbis-cookie-alone section 3.1)
    167  if (mCookieData.isSecure() && !potentiallyTrustworthy) {
    168    mResult = eRejectedSecureButNonHttps;
    169    return;
    170  }
    171 
    172  if (mCookieData.sameSite() == nsICookie::SAMESITE_UNSET) {
    173    bool laxByDefault =
    174        StaticPrefs::network_cookie_sameSite_laxByDefault() &&
    175        !nsContentUtils::IsURIInPrefList(
    176            aHostURI, "network.cookie.sameSite.laxByDefault.disabledHosts");
    177    if (laxByDefault) {
    178      mWarnings.mSameSiteLaxForced = true;
    179    } else if (StaticPrefs::
    180                   network_cookie_sameSite_laxByDefaultWarningsForBeta()) {
    181      mWarnings.mSameSiteLaxForcedForBeta = true;
    182    }
    183  }
    184 }
    185 
    186 void CookieValidation::ValidateInContextInternal(
    187    nsIURI* aHostURI, const nsACString& aBaseDomain, bool aRequireHostMatch,
    188    bool aFromHttp, bool aIsForeignAndNotAddon, bool aPartitionedOnly,
    189    bool aIsInPrivateBrowsing) {
    190  MOZ_ASSERT(mResult == eOK);
    191 
    192  ValidateForHostInternal(aHostURI, aBaseDomain, aRequireHostMatch, aFromHttp);
    193  if (mResult != eOK) {
    194    return;
    195  }
    196 
    197  // If the cookie is same-site but in a cross site context, browser must
    198  // ignore the cookie.
    199  bool laxByDefault =
    200      StaticPrefs::network_cookie_sameSite_laxByDefault() &&
    201      !nsContentUtils::IsURIInPrefList(
    202          aHostURI, "network.cookie.sameSite.laxByDefault.disabledHosts");
    203  uint32_t sameSite = mCookieData.sameSite();
    204  if (sameSite == nsICookie::SAMESITE_UNSET) {
    205    sameSite =
    206        laxByDefault ? nsICookie::SAMESITE_LAX : nsICookie::SAMESITE_NONE;
    207  }
    208 
    209  if (sameSite != nsICookie::SAMESITE_NONE && aIsForeignAndNotAddon) {
    210    mResult = eRejectedForNonSameSiteness;
    211    return;
    212  }
    213 
    214  // Ensure the partitioned cookie is set with the secure attribute if CHIPS
    215  // is enabled. This check should be part of ValidateInternal but it's not
    216  // because of bug 1965880.
    217  if (StaticPrefs::network_cookie_CHIPS_enabled() &&
    218      mCookieData.isPartitioned() && !mCookieData.isSecure()) {
    219    mResult = eRejectedPartitionedRequiresSecure;
    220    return;
    221  }
    222 }
    223 
    224 NS_IMETHODIMP
    225 CookieValidation::GetResult(nsICookieValidation::ValidationError* aRetval) {
    226  NS_ENSURE_ARG_POINTER(aRetval);
    227  *aRetval = mResult;
    228  return NS_OK;
    229 }
    230 
    231 // static
    232 bool CookieValidation::CheckDomain(const CookieStruct& aCookieData,
    233                                   nsIURI* aHostURI,
    234                                   const nsACString& aBaseDomain,
    235                                   bool aRequireHostMatch) {
    236  // Note: The logic in this function is mirrored in
    237  // toolkit/components/extensions/ext-cookies.js:checkSetCookiePermissions().
    238  // If it changes, please update that function, or file a bug for someone
    239  // else to do so.
    240 
    241  if (aCookieData.host().IsEmpty()) {
    242    return false;
    243  }
    244 
    245  // get host from aHostURI
    246  nsAutoCString hostFromURI;
    247  nsContentUtils::GetHostOrIPv6WithBrackets(aHostURI, hostFromURI);
    248 
    249  // check whether the host is either an IP address, an alias such as
    250  // 'localhost', an eTLD such as 'co.uk', or the empty string. in these
    251  // cases, require an exact string match for the domain, and leave the cookie
    252  // as a non-domain one. bug 105917 originally noted the requirement to deal
    253  // with IP addresses.
    254  if (aRequireHostMatch) {
    255    return hostFromURI.Equals(aCookieData.host());
    256  }
    257 
    258  nsCString cookieHost = aCookieData.host();
    259  // Tolerate leading '.' characters, but not if it's otherwise an empty host.
    260  if (aCookieData.host().Length() > 1 && aCookieData.host().First() == '.') {
    261    cookieHost.Cut(0, 1);
    262  }
    263 
    264  // ensure the proposed domain is derived from the base domain; and also
    265  // that the host domain is derived from the proposed domain (per RFC2109).
    266  if (CookieCommons::IsSubdomainOf(cookieHost, aBaseDomain) &&
    267      CookieCommons::IsSubdomainOf(hostFromURI, cookieHost)) {
    268    return true;
    269  }
    270 
    271  /*
    272   * note: RFC2109 section 4.3.2 requires that we check the following:
    273   * that the portion of host not in domain does not contain a dot.
    274   * this prevents hosts of the form x.y.co.nz from setting cookies in the
    275   * entire .co.nz domain. however, it's only a only a partial solution and
    276   * it breaks sites (IE doesn't enforce it), so we don't perform this check.
    277   */
    278  return false;
    279 }
    280 
    281 void CookieValidation::RetrieveErrorLogData(uint32_t* aFlags,
    282                                            nsACString& aCategory,
    283                                            nsACString& aKey,
    284                                            nsTArray<nsString>& aParams) const {
    285  MOZ_ASSERT(aFlags);
    286  MOZ_ASSERT(aParams.IsEmpty());
    287 
    288  *aFlags = nsIScriptError::errorFlag;
    289 
    290 #define SET_LOG_DATA(category, x) \
    291  aCategory = category;           \
    292  aKey = x;                       \
    293  aParams.AppendElement(NS_ConvertUTF8toUTF16(mCookieData.name()));
    294 
    295  switch (mResult) {
    296    case eOK:
    297      return;
    298 
    299    case eRejectedEmptyNameAndValue: {
    300      *aFlags = nsIScriptError::warningFlag;
    301      aCategory.Assign(CONSOLE_REJECTION_CATEGORY);
    302      aKey.Assign("CookieRejectedEmptyNameAndValue"_ns);
    303      return;
    304    }
    305 
    306    case eRejectedNoneRequiresSecure: {
    307      SET_LOG_DATA(CONSOLE_SAMESITE_CATEGORY,
    308                   "CookieRejectedNonRequiresSecure2"_ns);
    309      return;
    310    }
    311 
    312    case eRejectedPartitionedRequiresSecure: {
    313      SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY,
    314                   "CookieRejectedPartitionedRequiresSecure"_ns);
    315      return;
    316    }
    317 
    318    case eRejectedNameValueOversize: {
    319      *aFlags = nsIScriptError::warningFlag;
    320      aCategory.Assign(CONSOLE_OVERSIZE_CATEGORY);
    321      aKey.Assign("CookieOversize"_ns);
    322 
    323      aParams.AppendElement(NS_ConvertUTF8toUTF16(mCookieData.name()));
    324 
    325      nsString size;
    326      size.AppendInt(kMaxBytesPerCookie);
    327      aParams.AppendElement(size);
    328      return;
    329    }
    330 
    331    case eRejectedInvalidCharName: {
    332      SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY,
    333                   "CookieRejectedInvalidCharName"_ns);
    334      return;
    335    }
    336 
    337    case eRejectedInvalidCharValue: {
    338      SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY,
    339                   "CookieRejectedInvalidCharValue"_ns);
    340      return;
    341    }
    342 
    343    case eRejectedAttributePathOversize: {
    344      SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY,
    345                   "CookieRejectedAttributePathOversize"_ns);
    346      return;
    347    }
    348 
    349    case eRejectedAttributeDomainOversize: {
    350      SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY,
    351                   "CookieRejectedAttributeDomainOversize"_ns);
    352      return;
    353    }
    354 
    355    case eRejectedAttributeExpiryOversize: {
    356      SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY,
    357                   "CookieRejectedAttributeExpiryOversize"_ns);
    358      return;
    359    }
    360 
    361    case eRejectedInvalidPath: {
    362      SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, "CookieRejectedInvalidPath"_ns);
    363      return;
    364    }
    365 
    366    case eRejectedInvalidDomain: {
    367      SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY,
    368                   "CookieRejectedInvalidDomain"_ns);
    369      return;
    370    }
    371 
    372    case eRejectedInvalidPrefix: {
    373      SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY,
    374                   "CookieRejectedInvalidPrefix"_ns);
    375      return;
    376    }
    377 
    378    case eRejectedHttpOnlyButFromScript: {
    379      SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY,
    380                   "CookieRejectedHttpOnlyButFromScript"_ns);
    381      return;
    382    }
    383 
    384    case eRejectedSecureButNonHttps: {
    385      SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY,
    386                   "CookieRejectedSecureButNonHttps"_ns);
    387      return;
    388    }
    389 
    390    case eRejectedForNonSameSiteness: {
    391      SET_LOG_DATA(CONSOLE_SAMESITE_CATEGORY,
    392                   "CookieRejectedForNonSameSiteness"_ns);
    393      return;
    394    }
    395  }
    396 
    397 #undef SET_LOG_DATA
    398 }
    399 
    400 void CookieValidation::ReportErrorsAndWarnings(nsIConsoleReportCollector* aCRC,
    401                                               nsIURI* aHostURI) const {
    402  if (mResult != eOK) {
    403    uint32_t flags;
    404    nsAutoCString category;
    405    nsAutoCString key;
    406    nsTArray<nsString> params;
    407 
    408    RetrieveErrorLogData(&flags, category, key, params);
    409 
    410    CookieLogging::LogMessageToConsole(aCRC, aHostURI, flags, category, key,
    411                                       params);
    412    return;
    413  }
    414 
    415  if (mWarnings.mSameSiteNoneRequiresSecureForBeta) {
    416    CookieLogging::LogMessageToConsole(
    417        aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_SAMESITE_CATEGORY,
    418        "CookieRejectedNonRequiresSecureForBeta3"_ns,
    419        AutoTArray<nsString, 2>{NS_ConvertUTF8toUTF16(mCookieData.name()),
    420                                SAMESITE_MDN_URL});
    421  }
    422 
    423  if (mWarnings.mSameSiteLaxForced) {
    424    CookieLogging::LogMessageToConsole(
    425        aCRC, aHostURI, nsIScriptError::infoFlag, CONSOLE_SAMESITE_CATEGORY,
    426        "CookieLaxForced2"_ns,
    427        AutoTArray<nsString, 1>{NS_ConvertUTF8toUTF16(mCookieData.name())});
    428  }
    429 
    430  if (mWarnings.mSameSiteLaxForcedForBeta) {
    431    CookieLogging::LogMessageToConsole(
    432        aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_SAMESITE_CATEGORY,
    433        "CookieLaxForcedForBeta2"_ns,
    434        AutoTArray<nsString, 2>{NS_ConvertUTF8toUTF16(mCookieData.name()),
    435                                SAMESITE_MDN_URL});
    436  }
    437 }
    438 
    439 NS_IMETHODIMP
    440 CookieValidation::GetErrorString(nsAString& aResult) {
    441  if (mResult == eOK) {
    442    return NS_OK;
    443  }
    444 
    445  uint32_t flags;
    446  nsAutoCString category;
    447  nsAutoCString key;
    448  nsTArray<nsString> params;
    449 
    450  RetrieveErrorLogData(&flags, category, key, params);
    451 
    452  return nsContentUtils::FormatLocalizedString(
    453      nsContentUtils::eNECKO_PROPERTIES_en_US, key.get(), params, aResult);
    454 }
    455 
    456 // static
    457 bool CookieValidation::CheckNameAndValueSize(const CookieStruct& aCookieData) {
    458  // reject cookie if it's over the size limit, per RFC2109
    459  return (aCookieData.name().Length() + aCookieData.value().Length()) <=
    460         kMaxBytesPerCookie;
    461 }
    462 
    463 bool CookieValidation::CheckName(const CookieStruct& aCookieData) {
    464  if (!aCookieData.name().IsEmpty() && (aCookieData.name().First() == 0x20 ||
    465                                        aCookieData.name().Last() == 0x20)) {
    466    return false;
    467  }
    468 
    469  const char illegalNameCharacters[] = {
    470      0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C, 0x0D,
    471      0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19,
    472      0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x3B, 0x3D, 0x7F, 0x00};
    473 
    474  const auto* start = aCookieData.name().BeginReading();
    475  const auto* end = aCookieData.name().EndReading();
    476 
    477  auto charFilter = [&](unsigned char c) {
    478    if (StaticPrefs::network_cookie_blockUnicode() && c >= 0x80) {
    479      return true;
    480    }
    481    return std::find(std::begin(illegalNameCharacters),
    482                     std::end(illegalNameCharacters),
    483                     c) != std::end(illegalNameCharacters);
    484  };
    485 
    486  return std::find_if(start, end, charFilter) == end;
    487 }
    488 
    489 bool CookieValidation::CheckValue(const CookieStruct& aCookieData) {
    490  if (!aCookieData.value().IsEmpty() && (aCookieData.value().First() == 0x20 ||
    491                                         aCookieData.value().Last() == 0x20)) {
    492    return false;
    493  }
    494 
    495  // reject cookie if value contains an RFC 6265 disallowed character - see
    496  // https://bugzilla.mozilla.org/show_bug.cgi?id=1191423
    497  // NOTE: this is not the full set of characters disallowed by 6265 - notably
    498  // 0x09, 0x20, 0x22, 0x2C, and 0x5C are missing from this list.
    499  const char illegalCharacters[] = {
    500      0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C,
    501      0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
    502      0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x3B, 0x7F, 0x00};
    503 
    504  const auto* start = aCookieData.value().BeginReading();
    505  const auto* end = aCookieData.value().EndReading();
    506 
    507  bool shouldBlockEqualInNamelessCookie =
    508      aCookieData.name().IsEmpty() &&
    509      StaticPrefs::network_cookie_block_nameless_with_equal_char();
    510 
    511  auto charFilter = [&](unsigned char c) {
    512    if (StaticPrefs::network_cookie_blockUnicode() && c >= 0x80) {
    513      return true;
    514    }
    515 
    516    if (c == '=' && shouldBlockEqualInNamelessCookie) {
    517      return true;
    518    }
    519 
    520    return std::find(std::begin(illegalCharacters), std::end(illegalCharacters),
    521                     c) != std::end(illegalCharacters);
    522  };
    523 
    524  return std::find_if(start, end, charFilter) == end;
    525 }