Bug 1557346 - Limit referer header length r=ckerschb
☠☠ backed out by 450801fafd10 ☠ ☠
authorThomas Nguyen <tnguyen@mozilla.com>
Tue, 09 Jul 2019 14:44:27 +0000
changeset 481916 9ba600ae3c02a21a273596f2c98a2c47b0ee7c6e
parent 481915 aa07814cd7c4058bb500189dc4eb945512910a52
child 481917 06d4b70144ff6e923f61b06d4164b9eda977c8cb
push id113647
push useraciure@mozilla.com
push dateWed, 10 Jul 2019 09:46:39 +0000
treeherdermozilla-inbound@f3a387c13e2c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersckerschb
bugs1557346
milestone70.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1557346 - Limit referer header length r=ckerschb Differential Revision: https://phabricator.services.mozilla.com/D35990
dom/locales/en-US/chrome/security/security.properties
dom/security/ReferrerInfo.cpp
dom/security/ReferrerInfo.h
modules/libpref/init/all.js
netwerk/test/unit/test_referrer.js
--- a/dom/locales/en-US/chrome/security/security.properties
+++ b/dom/locales/en-US/chrome/security/security.properties
@@ -115,8 +115,13 @@ ReportingHeaderInvalidEndpoint=Reporting Header: ignoring invalid endpoint for item named “%S”.
 # LOCALIZATION NOTE(ReportingHeaderInvalidURLEndpoint): %1$S is the invalid URL, %2$S is the group name
 ReportingHeaderInvalidURLEndpoint=Reporting Header: ignoring invalid endpoint URL “%1$S” for item named “%2$S”.
 
 FeaturePolicyUnsupportedFeatureName=Feature Policy: Skipping unsupported feature name “%S”.
 # TODO: would be nice to add a link to the Feature-Policy MDN documentation here. See bug 1449501
 FeaturePolicyInvalidEmptyAllowValue= Feature Policy: Skipping empty allow list for feature: “%S”.
 # TODO: would be nice to add a link to the Feature-Policy MDN documentation here. See bug 1449501
 FeaturePolicyInvalidAllowValue=Feature Policy: Skipping unsupported allow value “%S”.
+
+# LOCALIZATION NOTE: "%1$S" is the limitation length (bytes) of referrer URI, "%2$S" is the origin of the referrer URI.
+ReferrerLengthOverLimitation=HTTP Referrer header: Length is over "%1$S" bytes limit - stripping referrer header down to origin: “%2$S”
+# LOCALIZATION NOTE: "%1$S" is the limitation length (bytes) of referrer URI, "%2$S" is the origin of the referrer URI.
+ReferrerOriginLengthOverLimitation=HTTP Referrer header: Length of origin within referrer is over "%1$S" bytes limit - removing referrer with origin "%2$S".
--- a/dom/security/ReferrerInfo.cpp
+++ b/dom/security/ReferrerInfo.cpp
@@ -6,17 +6,16 @@
 
 #include "nsIClassInfoImpl.h"
 #include "nsICookieService.h"
 #include "nsIHttpChannel.h"
 #include "nsIObjectInputStream.h"
 #include "nsIObjectOutputStream.h"
 #include "nsIURIFixup.h"
 #include "nsIURL.h"
-#include "nsIURIMutator.h"
 
 #include "nsWhitespaceTokenizer.h"
 #include "nsAlgorithm.h"
 #include "nsContentUtils.h"
 #include "ReferrerInfo.h"
 
 #include "mozilla/AntiTrackingCommon.h"
 #include "mozilla/net/CookieSettings.h"
@@ -38,16 +37,18 @@ NS_IMPL_CLASSINFO(ReferrerInfo, nullptr,
 
 NS_IMPL_ISUPPORTS_CI(ReferrerInfo, nsIReferrerInfo, nsISerializable)
 
 #define DEFAULT_RP 3
 #define DEFAULT_TRACKER_RP 3
 #define DEFAULT_PRIVATE_RP 2
 #define DEFAULT_TRACKER_PRIVATE_RP 2
 
+#define DEFAULT_REFERRER_HEADER_LENGTH_LIMIT 4096
+
 #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
 
@@ -57,29 +58,33 @@ static uint32_t defaultPrivateRp = DEFAU
 static uint32_t defaultTrackerPrivateRp = DEFAULT_TRACKER_PRIVATE_RP;
 
 static bool sUserSpoofReferrerSource = false;
 static bool sUserHideOnionReferrerSource = false;
 static uint32_t sUserReferrerSendingPolicy = 0;
 static uint32_t sUserXOriginSendingPolicy = 0;
 static uint32_t sUserTrimmingPolicy = 0;
 static uint32_t sUserXOriginTrimmingPolicy = 0;
+static uint32_t sReferrerHeaderLimit = DEFAULT_REFERRER_HEADER_LENGTH_LIMIT;
 
 static void CachePreferrenceValue() {
   static bool sPrefCached = false;
   if (sPrefCached) {
     return;
   }
 
   Preferences::AddBoolVarCache(&sUserSpoofReferrerSource,
                                "network.http.referer.spoofSource");
   Preferences::AddBoolVarCache(&sUserHideOnionReferrerSource,
                                "network.http.referer.hideOnionSource");
   Preferences::AddUintVarCache(&sUserReferrerSendingPolicy,
                                "network.http.sendRefererHeader");
+  Preferences::AddUintVarCache(&sReferrerHeaderLimit,
+                               "network.http.referer.referrerLengthLimit",
+                               DEFAULT_REFERRER_HEADER_LENGTH_LIMIT);
   sUserReferrerSendingPolicy =
       clamped<uint32_t>(sUserReferrerSendingPolicy, MIN_REFERRER_SENDING_POLICY,
                         MAX_REFERRER_SENDING_POLICY);
 
   Preferences::AddUintVarCache(&sUserXOriginSendingPolicy,
                                "network.http.referer.XOriginPolicy");
   sUserXOriginSendingPolicy = clamped<uint32_t>(
       sUserXOriginSendingPolicy, MIN_CROSS_ORIGIN_SENDING_POLICY,
@@ -477,80 +482,165 @@ ReferrerInfo::TrimmingPolicy ReferrerInf
     default:
       MOZ_ASSERT_UNREACHABLE("Unexpected value");
       break;
   }
 
   return static_cast<TrimmingPolicy>(trimmingPolicy);
 }
 
-nsresult ReferrerInfo::TrimReferrerWithPolicy(nsCOMPtr<nsIURI>& aReferrer,
-                                              TrimmingPolicy aTrimmingPolicy,
-                                              nsACString& aResult) const {
-  if (aTrimmingPolicy == TrimmingPolicy::ePolicyFullURI) {
-    // use the full URI
-    return aReferrer->GetAsciiSpec(aResult);
+nsresult ReferrerInfo::LimitReferrerLength(
+    nsIHttpChannel* aChannel, nsIURI* aReferrer, TrimmingPolicy aTrimmingPolicy,
+    nsACString& aInAndOutTrimmedReferrer) const {
+  if (!sReferrerHeaderLimit) {
+    return NS_OK;
+  }
+
+  if (aInAndOutTrimmedReferrer.Length() <= sReferrerHeaderLimit) {
+    return NS_OK;
   }
 
-  // All output strings start with: scheme+host+port
-  // We want the IDN-normalized PrePath.  That's not something currently
+  nsAutoString referrerLengthLimit;
+  referrerLengthLimit.AppendInt(sReferrerHeaderLimit);
+  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() <= sReferrerHeaderLimit) {
+      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 ourselves from scheme+AsciiHostPort
+  // 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);
 
-  switch (aTrimmingPolicy) {
-    case 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);
-        rv = NS_MutateURI(url)
-                 .SetQuery(EmptyCString())
-                 .SetRef(EmptyCString())
-                 .Finalize(aReferrer);
-        if (NS_WARN_IF(NS_FAILED(rv))) {
-          return rv;
-        }
-        break;
-      }
-      // No URL, so fall through to truncating the path and any query/ref off
-      // as well.
-    }
-      MOZ_FALLTHROUGH;
-    default:  // (User Pref limited to [0,2])
-    case TrimmingPolicy::ePolicySchemeHostPort:
-      aResult.AppendLiteral("/");
-      // This nukes any query/ref present as well in the case of nsStandardURL
-      rv = NS_MutateURI(aReferrer)
-               .SetPathQueryRef(EmptyCString())
-               .Finalize(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;
       }
-      break;
+
+      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;
+}
+
+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;
   }
 
-  return NS_OK;
+  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, NS_LITERAL_CSTRING("Security"),
+      windowID, uri);
+  Unused << NS_WARN_IF(NS_FAILED(rv));
 }
+
 ReferrerInfo::ReferrerInfo()
     : mOriginalReferrer(nullptr),
       mPolicy(mozilla::net::RP_Unset),
       mSendReferrer(true),
       mInitialized(false),
       mOverridePolicyByDefault(false),
       mComputedReferrer(Maybe<nsCString>()) {}
 
@@ -902,25 +992,34 @@ nsresult ReferrerInfo::ComputeReferrer(n
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   referrer = exposableURI;
 
   TrimmingPolicy trimmingPolicy = ComputeTrimmingPolicy(aChannel);
 
-  nsAutoCString referrerSpec;
-  rv = TrimReferrerWithPolicy(referrer, trimmingPolicy, referrerSpec);
+  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;
   }
 
-  // finally, remember the referrer URI.
+  rv = LimitReferrerLength(aChannel, referrer, trimmingPolicy, trimmedReferrer);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  // finally, remember the referrer spec.
   mComputedReferrer.reset();
-  mComputedReferrer.emplace(referrerSpec);
+  mComputedReferrer.emplace(trimmedReferrer);
 
   return NS_OK;
 }
 
 /* ===== nsISerializable implementation ====== */
 
 NS_IMETHODIMP
 ReferrerInfo::Read(nsIObjectInputStream* aStream) {
--- a/dom/security/ReferrerInfo.h
+++ b/dom/security/ReferrerInfo.h
@@ -251,22 +251,63 @@ class ReferrerInfo : public nsIReferrerI
   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, ftp)
+   *
+   *  @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(nsCOMPtr<nsIURI>& aReferrer,
+  nsresult TrimReferrerWithPolicy(nsIURI* aReferrer,
                                   TrimmingPolicy aTrimmingPolicy,
                                   nsACString& aResult) 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;
+
+  /*
+   * Write message to the error console
+   */
+  void LogMessageToConsole(nsIHttpChannel* aChannel, const char* aMsg,
+                           const nsTArray<nsString>& aParams) const;
+
   nsCOMPtr<nsIURI> mOriginalReferrer;
 
   uint32_t mPolicy;
 
   // Indicates if the referrer should be sent or not even when it's available
   // (default is true).
   bool mSendReferrer;
 
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -1600,16 +1600,19 @@ pref("network.http.referer.spoofSource",
 // false=allow onion referer, true=hide onion referer (use empty referer)
 pref("network.http.referer.hideOnionSource", false);
 // 0=full URI, 1=scheme+host+port+path, 2=scheme+host+port
 pref("network.http.referer.trimmingPolicy", 0);
 // 0=full URI, 1=scheme+host+port+path, 2=scheme+host+port
 pref("network.http.referer.XOriginTrimmingPolicy", 0);
 // 0=always send, 1=send iff base domains match, 2=send iff hosts match
 pref("network.http.referer.XOriginPolicy", 0);
+// The maximum allowed length for a referrer header - 4096 default
+// 0 means no limit.
+pref("network.http.referer.referrerLengthLimit", 4096);
 
 // Include an origin header on non-GET and non-HEAD requests regardless of CORS
 // 0=never send, 1=send when same-origin only, 2=always send
 pref("network.http.sendOriginHeader", 0);
 
 // Maximum number of consecutive redirects before aborting.
 pref("network.http.redirection-limit", 20);
 
--- a/netwerk/test/unit/test_referrer.js
+++ b/netwerk/test/unit/test_referrer.js
@@ -207,16 +207,29 @@ function run_test() {
   );
   prefs.setIntPref("network.http.referer.XOriginTrimmingPolicy", 0);
   // test that anchor is lopped off in ordinary case
   Assert.equal(
     getTestReferrer(server_uri, referer_uri_2_anchor),
     referer_uri_2
   );
 
+  // test referrer length limitation
+  // referer_uri's length is 27 and origin's length is 23
+  prefs.setIntPref("network.http.referer.referrerLengthLimit", 27);
+  Assert.equal(getTestReferrer(server_uri, referer_uri), referer_uri);
+  prefs.setIntPref("network.http.referer.referrerLengthLimit", 26);
+  Assert.equal(getTestReferrer(server_uri, referer_uri), "http://foo.example.com/");
+  prefs.setIntPref("network.http.referer.referrerLengthLimit", 22);
+  Assert.equal(getTestReferrer(server_uri, referer_uri), null);
+  prefs.setIntPref("network.http.referer.referrerLengthLimit", 0);
+  Assert.equal(getTestReferrer(server_uri, referer_uri), referer_uri);
+  prefs.setIntPref("network.http.referer.referrerLengthLimit", 4096);
+  Assert.equal(getTestReferrer(server_uri, referer_uri), referer_uri);
+
   // combination test: send spoofed path-only when hosts match
   var combo_referer_uri = "http://blah.foo.com/path?q=hot";
   var dest_uri = "http://blah.foo.com:9999/spoofedpath?q=bad";
   prefs.setIntPref("network.http.referer.trimmingPolicy", 1);
   prefs.setBoolPref("network.http.referer.spoofSource", true);
   prefs.setIntPref("network.http.referer.XOriginPolicy", 2);
   Assert.equal(
     getTestReferrer(dest_uri, combo_referer_uri),