Source code

Revision control

Copy as Markdown

Other Tools

/* 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 "URLQueryStringStripper.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/StaticPrefs_privacy.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/Unused.h"
#include "mozilla/Telemetry.h"
#include "nsEffectiveTLDService.h"
#include "nsISupportsImpl.h"
#include "nsIURI.h"
#include "nsIURIMutator.h"
#include "nsUnicharUtils.h"
#include "nsURLHelper.h"
#include "nsNetUtil.h"
#include "mozilla/dom/StripOnShareRuleBinding.h"
namespace {
mozilla::StaticRefPtr<mozilla::URLQueryStringStripper> gQueryStringStripper;
static const char kQueryStrippingEnabledPref[] =
"privacy.query_stripping.enabled";
static const char kQueryStrippingEnabledPBMPref[] =
"privacy.query_stripping.enabled.pbmode";
static const char kQueryStrippingOnShareEnabledPref[] =
"privacy.query_stripping.strip_on_share.enabled";
} // namespace
namespace mozilla {
NS_IMPL_ISUPPORTS(URLQueryStringStripper, nsIObserver,
nsIURLQueryStringStripper, nsIURLQueryStrippingListObserver)
// static
already_AddRefed<URLQueryStringStripper>
URLQueryStringStripper::GetSingleton() {
if (!gQueryStringStripper) {
gQueryStringStripper = new URLQueryStringStripper();
// Check initial pref state and enable service. We can pass nullptr, because
// OnPrefChange doesn't rely on the args.
URLQueryStringStripper::OnPrefChange(nullptr, nullptr);
RunOnShutdown(
[&] {
DebugOnly<nsresult> rv = gQueryStringStripper->Shutdown();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"URLQueryStringStripper::Shutdown failed");
gQueryStringStripper = nullptr;
},
ShutdownPhase::XPCOMShutdown);
}
return do_AddRef(gQueryStringStripper);
}
URLQueryStringStripper::URLQueryStringStripper() {
mIsInitialized = false;
nsresult rv = Preferences::RegisterCallback(
&URLQueryStringStripper::OnPrefChange, kQueryStrippingEnabledPBMPref);
NS_ENSURE_SUCCESS_VOID(rv);
rv = Preferences::RegisterCallback(&URLQueryStringStripper::OnPrefChange,
kQueryStrippingEnabledPref);
rv = Preferences::RegisterCallback(&URLQueryStringStripper::OnPrefChange,
kQueryStrippingOnShareEnabledPref);
NS_ENSURE_SUCCESS_VOID(rv);
}
NS_IMETHODIMP
URLQueryStringStripper::StripForCopyOrShare(nsIURI* aURI,
nsIURI** strippedURI) {
if (!StaticPrefs::privacy_query_stripping_strip_on_share_enabled()) {
return NS_ERROR_NOT_AVAILABLE;
}
NS_ENSURE_ARG_POINTER(aURI);
NS_ENSURE_ARG_POINTER(strippedURI);
int aStripCount = 0;
nsresult rv =
StripForCopyOrShareInternal(aURI, strippedURI, aStripCount, false);
NS_ENSURE_SUCCESS(rv, rv);
Telemetry::Accumulate(Telemetry::STRIP_ON_SHARE_PARAMS_REMOVED, aStripCount);
if (!aStripCount) {
return NS_OK;
}
// To calculate difference in length of the URL
// after stripping occurs for Telemetry
nsAutoCString specOriginalURI;
nsAutoCString specStrippedURI;
rv = aURI->GetDisplaySpec(specOriginalURI);
NS_ENSURE_SUCCESS(rv, rv);
MOZ_ASSERT(*strippedURI);
rv = (*strippedURI)->GetDisplaySpec(specStrippedURI);
NS_ENSURE_SUCCESS(rv, rv);
uint32_t lengthDiff = specOriginalURI.Length() - specStrippedURI.Length();
Telemetry::Accumulate(Telemetry::STRIP_ON_SHARE_LENGTH_DECREASE, lengthDiff);
return NS_OK;
}
NS_IMETHODIMP
URLQueryStringStripper::CanStripForShare(nsIURI* aURI, bool* aCanStrip) {
NS_ENSURE_ARG_POINTER(aURI);
NS_ENSURE_ARG_POINTER(aCanStrip);
*aCanStrip = false;
return CanStripForCopyOrShareInternal(aURI, aCanStrip, true);
}
NS_IMETHODIMP
URLQueryStringStripper::Strip(nsIURI* aURI, bool aIsPBM, nsIURI** aOutput,
uint32_t* aStripCount) {
NS_ENSURE_ARG_POINTER(aURI);
NS_ENSURE_ARG_POINTER(aOutput);
NS_ENSURE_ARG_POINTER(aStripCount);
*aStripCount = 0;
if (aIsPBM) {
if (!StaticPrefs::privacy_query_stripping_enabled_pbmode()) {
return NS_OK;
}
} else {
if (!StaticPrefs::privacy_query_stripping_enabled()) {
return NS_OK;
}
}
if (CheckAllowList(aURI)) {
return NS_OK;
}
return StripQueryString(aURI, aOutput, aStripCount);
}
// static
void URLQueryStringStripper::OnPrefChange(const char* aPref, void* aData) {
MOZ_ASSERT(gQueryStringStripper);
bool prefEnablesComponent =
StaticPrefs::privacy_query_stripping_enabled() ||
StaticPrefs::privacy_query_stripping_enabled_pbmode() ||
StaticPrefs::privacy_query_stripping_strip_on_share_enabled();
nsresult rv;
if (prefEnablesComponent) {
rv = gQueryStringStripper->Init();
} else {
rv = gQueryStringStripper->Shutdown();
}
NS_ENSURE_SUCCESS_VOID(rv);
}
nsresult URLQueryStringStripper::Init() {
nsresult rv;
if (mIsInitialized) {
rv = gQueryStringStripper->ManageObservers();
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
mIsInitialized = true;
mListService = do_GetService("@mozilla.org/query-stripping-list-service;1");
NS_ENSURE_TRUE(mListService, NS_ERROR_FAILURE);
rv = gQueryStringStripper->ManageObservers();
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
// (Un)registers a QPS/Strip-on-share observer according to the QPS prefs states
// and the strip-on-share pref state. This is called whenever one of the three
// prefs changes, to ensure that we are not observing one of the lists although
// the corresponding feature is not turned on.
nsresult URLQueryStringStripper::ManageObservers() {
MOZ_ASSERT(mListService);
nsresult rv;
// Register QPS observer.
// We are not listening to QPS but the feature is on, register a listener.
if (!mObservingQPS) {
if (StaticPrefs::privacy_query_stripping_enabled() ||
StaticPrefs::privacy_query_stripping_enabled_pbmode()) {
rv = mListService->RegisterAndRunObserver(gQueryStringStripper);
NS_ENSURE_SUCCESS(rv, rv);
mObservingQPS = true;
}
} else {
// Unregister QPS observer.
// We are listening to QPS but the feature is off, unregister.
if (!StaticPrefs::privacy_query_stripping_enabled() &&
!StaticPrefs::privacy_query_stripping_enabled_pbmode()) {
// Clean up QPS lists.
mList.Clear();
mAllowList.Clear();
rv = mListService->UnregisterObserver(this);
NS_ENSURE_SUCCESS(rv, rv);
mObservingQPS = false;
}
}
// Register Strip on Share observer.
// We are not listening to strip-on-share but the feature is on, register an
// Observer.
if (!mObservingStripOnShare) {
if (StaticPrefs::privacy_query_stripping_strip_on_share_enabled()) {
rv = mListService->RegisterAndRunObserverStripOnShare(
gQueryStringStripper);
NS_ENSURE_SUCCESS(rv, rv);
mObservingStripOnShare = true;
}
} else {
// Unregister Strip on Share observer.
// We are listening to strip-on-share but the feature is off, unregister.
if (!StaticPrefs::privacy_query_stripping_strip_on_share_enabled()) {
// Clean up strip-on-share list
mStripOnShareMap.Clear();
rv = mListService->UnregisterStripOnShareObserver(this);
NS_ENSURE_SUCCESS(rv, rv);
mObservingStripOnShare = false;
}
}
return NS_OK;
}
nsresult URLQueryStringStripper::Shutdown() {
if (!mIsInitialized) {
return NS_OK;
}
nsresult rv = gQueryStringStripper->ManageObservers();
NS_ENSURE_SUCCESS(rv, rv);
mIsInitialized = false;
mListService = nullptr;
return NS_OK;
}
nsresult URLQueryStringStripper::StripQueryString(nsIURI* aURI,
nsIURI** aOutput,
uint32_t* aStripCount) {
NS_ENSURE_ARG_POINTER(aURI);
NS_ENSURE_ARG_POINTER(aOutput);
NS_ENSURE_ARG_POINTER(aStripCount);
*aStripCount = 0;
nsCOMPtr<nsIURI> uri(aURI);
nsAutoCString query;
nsresult rv = aURI->GetQuery(query);
NS_ENSURE_SUCCESS(rv, rv);
// We don't need to do anything if there is no query string.
if (query.IsEmpty()) {
return NS_OK;
}
URLParams params;
URLParams::Parse(query, false, [&](nsCString&& name, nsCString&& value) {
nsAutoCString lowerCaseName;
ToLowerCase(name, lowerCaseName);
if (mList.Contains(lowerCaseName)) {
*aStripCount += 1;
// Count how often a specific query param is stripped. For privacy reasons
// this will only count query params listed in the Histogram definition.
// Calls for any other query params will be discarded.
nsAutoCString telemetryLabel("param_");
telemetryLabel.Append(lowerCaseName);
Telemetry::AccumulateCategorical(
Telemetry::QUERY_STRIPPING_COUNT_BY_PARAM, telemetryLabel);
return true;
}
params.Append(name, value);
return true;
});
// Return if there is no parameter has been stripped.
if (!*aStripCount) {
return NS_OK;
}
nsAutoCString newQuery;
params.Serialize(newQuery, false);
Unused << NS_MutateURI(uri).SetQuery(newQuery).Finalize(aOutput);
return NS_OK;
}
bool URLQueryStringStripper::CheckAllowList(nsIURI* aURI) {
MOZ_ASSERT(aURI);
// Get the site(eTLD+1) from the URI.
nsAutoCString baseDomain;
nsresult rv =
nsEffectiveTLDService::GetInstance()->GetBaseDomain(aURI, 0, baseDomain);
if (rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
return false;
}
NS_ENSURE_SUCCESS(rv, false);
return mAllowList.Contains(baseDomain);
}
void URLQueryStringStripper::PopulateStripList(const nsACString& aList) {
mList.Clear();
for (const nsACString& item : aList.Split(' ')) {
mList.Insert(item);
}
}
void URLQueryStringStripper::PopulateAllowList(const nsACString& aList) {
mAllowList.Clear();
for (const nsACString& item : aList.Split(',')) {
mAllowList.Insert(item);
}
}
NS_IMETHODIMP
URLQueryStringStripper::OnQueryStrippingListUpdate(
const nsACString& aStripList, const nsACString& aAllowList) {
PopulateStripList(aStripList);
PopulateAllowList(aAllowList);
return NS_OK;
}
NS_IMETHODIMP
URLQueryStringStripper::OnStripOnShareUpdate(const nsTArray<nsString>& aArgs,
JSContext* aCx) {
for (const auto& ruleString : aArgs) {
dom::StripRule rule;
if (NS_WARN_IF(!rule.Init(ruleString))) {
// Skipping malformed rules
continue;
}
for (const auto& topLevelSite : rule.mTopLevelSites) {
mStripOnShareMap.InsertOrUpdate(topLevelSite, rule);
}
}
return NS_OK;
}
// static
NS_IMETHODIMP
URLQueryStringStripper::TestGetStripList(nsACString& aStripList) {
aStripList.Truncate();
StringJoinAppend(
aStripList, " "_ns, mList,
[](auto& aResult, const auto& aValue) { aResult.Append(aValue); });
return NS_OK;
}
/* nsIObserver */
NS_IMETHODIMP
URLQueryStringStripper::Observe(nsISupports*, const char* aTopic,
const char16_t*) {
// Since this class is created at profile-after-change by the Category
// Manager, it's expected to implement nsIObserver; however, we have nothing
// interesting to do here.
MOZ_ASSERT(strcmp(aTopic, "profile-after-change") == 0);
return NS_OK;
}
nsresult URLQueryStringStripper::StripForCopyOrShareInternal(
nsIURI* aURI, nsIURI** strippedURI, int& aStripCount,
bool aStripNestedURIs) {
nsAutoCString query;
nsresult rv = aURI->GetQuery(query);
NS_ENSURE_SUCCESS(rv, rv);
// We don't need to do anything if there is no query string.
if (query.IsEmpty()) {
Telemetry::Accumulate(Telemetry::STRIP_ON_SHARE_PARAMS_REMOVED, 0);
return NS_OK;
}
nsAutoCString host;
rv = aURI->GetHost(host);
NS_ENSURE_SUCCESS(rv, rv);
URLParams params;
URLParams::Parse(query, false, [&](nsCString&& name, nsCString&& value) {
nsAutoCString lowerCaseName;
ToLowerCase(name, lowerCaseName);
// Look through the global rules.
dom::StripRule globalRule;
bool keyExists = mStripOnShareMap.Get("*"_ns, &globalRule);
// There should always be a global rule.
MOZ_ASSERT(keyExists);
// Look through the global rules.
for (const auto& param : globalRule.mQueryParams) {
if (param == lowerCaseName) {
aStripCount++;
return true;
}
}
// Check for site specific rules.
dom::StripRule siteSpecificRule;
keyExists = mStripOnShareMap.Get(host, &siteSpecificRule);
if (keyExists) {
for (const auto& param : siteSpecificRule.mQueryParams) {
if (param == lowerCaseName) {
aStripCount++;
return true;
}
}
}
// Only if it is top layer of the recursion then it
// checks if the value of the query parameter is a valid URI
// if not then it gets added back to the query, if it is then
// it gets passed back into this method but with the recursive
// stripping flag set to true
if (!aStripNestedURIs) {
nsAutoCString decodeValue;
URLParams::DecodeString(value, decodeValue);
nsCOMPtr<nsIURI> nestedURI;
rv = NS_NewURI(getter_AddRefs(nestedURI), decodeValue);
if (NS_WARN_IF(NS_FAILED(rv))) {
params.Append(name, value);
return true;
}
nsCOMPtr<nsIURI> strippedNestedURI;
rv = StripForCopyOrShareInternal(
nestedURI, getter_AddRefs(strippedNestedURI), aStripCount, true);
if (NS_WARN_IF(NS_FAILED(rv))) {
params.Append(name, value);
return true;
}
if (!strippedNestedURI) {
params.Append(name, value);
return true;
}
nsAutoCString nestedURIString;
rv = strippedNestedURI->GetSpec(nestedURIString);
if (NS_WARN_IF(NS_FAILED(rv))) {
params.Append(name, value);
return true;
}
// Encodes URI
nsAutoCString encodedURI;
URLParams::SerializeString(nestedURIString, encodedURI);
params.Append(name, encodedURI);
return true;
}
params.Append(name, value);
return true;
});
// Returns null for strippedURI if no query params have been stripped.
if (!aStripCount) {
return NS_OK;
}
nsAutoCString newQuery;
params.Serialize(newQuery, false);
return NS_MutateURI(aURI).SetQuery(newQuery).Finalize(strippedURI);
}
nsresult URLQueryStringStripper::CanStripForCopyOrShareInternal(
nsIURI* aURI, bool* aCanStrip, bool aStripNestedURIs) {
*aCanStrip = false;
nsAutoCString query;
nsresult rv = aURI->GetQuery(query);
NS_ENSURE_SUCCESS(rv, rv);
// TODO(Bug 1919551): deduplicate code with StripForCopyOrShare
// We don't need to do anything if there is no query string.
if (query.IsEmpty()) {
return NS_OK;
}
nsAutoCString host;
rv = aURI->GetHost(host);
NS_ENSURE_SUCCESS(rv, rv);
URLParams params;
URLParams::Parse(query, false, [&](nsCString&& name, nsCString&& value) {
nsAutoCString lowerCaseName;
ToLowerCase(name, lowerCaseName);
// Look through the global rules.
dom::StripRule globalRule;
bool keyExists = mStripOnShareMap.Get("*"_ns, &globalRule);
// There should always be a global rule.
MOZ_ASSERT(keyExists);
// Look through the global rules.
for (const auto& param : globalRule.mQueryParams) {
if (param == lowerCaseName) {
*aCanStrip = true;
return false;
}
}
// Check for site specific rules.
dom::StripRule siteSpecificRule;
keyExists = mStripOnShareMap.Get(host, &siteSpecificRule);
if (keyExists) {
for (const auto& param : siteSpecificRule.mQueryParams) {
if (param == lowerCaseName) {
*aCanStrip = true;
return false;
}
}
}
// Also strip URIs nested in query params.
// The aStripNestedURIs flag is used to ensure we only go
// one level deep. Stripping highly nested URIs is not supported.
if (aStripNestedURIs) {
// Ignore errors when trying to strip potential nested URLs. It is
// expected to fail on non-URLs.
nsresult nestedRv;
nsAutoCString decodeValue;
URLParams::DecodeString(value, decodeValue);
nsCOMPtr<nsIURI> nestedURI;
nestedRv = NS_NewURI(getter_AddRefs(nestedURI), decodeValue);
if (NS_FAILED(nestedRv)) {
return true;
}
nestedRv = CanStripForCopyOrShareInternal(nestedURI, aCanStrip, false);
if (NS_FAILED(nestedRv)) {
return true;
}
// If we found a query param to strip skip iterating over the remaining
// ones (we return greedily).
if (*aCanStrip) {
return false;
}
}
return true;
});
return rv;
}
} // namespace mozilla