/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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 "mozilla/dom/ReportingHeader.h" #include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject #include "js/JSON.h" #include "js/PropertyAndElement.h" // JS_GetElement #include "mozilla/dom/ReportingBinding.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/SimpleGlobalObject.h" #include "mozilla/ipc/BackgroundUtils.h" #include "mozilla/OriginAttributes.h" #include "mozilla/Services.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/StaticPtr.h" #include "nsCOMPtr.h" #include "nsContentUtils.h" #include "nsIEffectiveTLDService.h" #include "nsIHttpChannel.h" #include "nsIHttpProtocolHandler.h" #include "nsIObserverService.h" #include "nsIPrincipal.h" #include "nsIRandomGenerator.h" #include "nsIScriptError.h" #include "nsNetUtil.h" #include "nsXULAppAPI.h" #define REPORTING_PURGE_ALL "reporting:purge-all" #define REPORTING_PURGE_HOST "reporting:purge-host" namespace mozilla::dom { namespace { StaticRefPtr gReporting; } // namespace /* static */ void ReportingHeader::Initialize() { MOZ_ASSERT(!gReporting); MOZ_ASSERT(NS_IsMainThread()); if (!XRE_IsParentProcess()) { return; } RefPtr service = new ReportingHeader(); nsCOMPtr obs = services::GetObserverService(); if (NS_WARN_IF(!obs)) { return; } obs->AddObserver(service, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC, false); obs->AddObserver(service, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); obs->AddObserver(service, "clear-origin-attributes-data", false); obs->AddObserver(service, REPORTING_PURGE_HOST, false); obs->AddObserver(service, REPORTING_PURGE_ALL, false); gReporting = service; } /* static */ void ReportingHeader::Shutdown() { MOZ_ASSERT(NS_IsMainThread()); if (!gReporting) { return; } RefPtr service = gReporting; gReporting = nullptr; if (service->mCleanupTimer) { service->mCleanupTimer->Cancel(); service->mCleanupTimer = nullptr; } nsCOMPtr obs = services::GetObserverService(); if (NS_WARN_IF(!obs)) { return; } obs->RemoveObserver(service, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC); obs->RemoveObserver(service, NS_XPCOM_SHUTDOWN_OBSERVER_ID); obs->RemoveObserver(service, "clear-origin-attributes-data"); obs->RemoveObserver(service, REPORTING_PURGE_HOST); obs->RemoveObserver(service, REPORTING_PURGE_ALL); } ReportingHeader::ReportingHeader() = default; ReportingHeader::~ReportingHeader() = default; NS_IMETHODIMP ReportingHeader::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { Shutdown(); return NS_OK; } // Pref disabled. if (!StaticPrefs::dom_reporting_header_enabled()) { return NS_OK; } if (!strcmp(aTopic, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC)) { nsCOMPtr channel = do_QueryInterface(aSubject); if (NS_WARN_IF(!channel)) { return NS_OK; } ReportingFromChannel(channel); return NS_OK; } if (!strcmp(aTopic, REPORTING_PURGE_HOST)) { RemoveOriginsFromHost(nsDependentString(aData)); return NS_OK; } if (!strcmp(aTopic, "clear-origin-attributes-data")) { OriginAttributesPattern pattern; if (!pattern.Init(nsDependentString(aData))) { NS_ERROR("Cannot parse origin attributes pattern"); return NS_ERROR_FAILURE; } RemoveOriginsFromOriginAttributesPattern(pattern); return NS_OK; } if (!strcmp(aTopic, REPORTING_PURGE_ALL)) { RemoveOrigins(); return NS_OK; } return NS_ERROR_FAILURE; } void ReportingHeader::ReportingFromChannel(nsIHttpChannel* aChannel) { MOZ_ASSERT(aChannel); if (!StaticPrefs::dom_reporting_header_enabled()) { return; } // We want to use the final URI to check if Report-To should be allowed or // not. nsCOMPtr uri; nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); if (NS_WARN_IF(NS_FAILED(rv))) { return; } if (!IsSecureURI(uri)) { return; } if (NS_UsePrivateBrowsing(aChannel)) { return; } nsAutoCString headerValue; rv = aChannel->GetResponseHeader("Report-To"_ns, headerValue); if (NS_FAILED(rv)) { return; } nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); if (NS_WARN_IF(!ssm)) { return; } nsCOMPtr principal; rv = ssm->GetChannelURIPrincipal(aChannel, getter_AddRefs(principal)); if (NS_WARN_IF(NS_FAILED(rv)) || !principal) { return; } nsAutoCString origin; rv = principal->GetOrigin(origin); if (NS_WARN_IF(NS_FAILED(rv))) { return; } UniquePtr client = ParseHeader(aChannel, uri, headerValue); if (!client) { return; } // Here we override the previous data. mOrigins.InsertOrUpdate(origin, std::move(client)); MaybeCreateCleanupTimer(); } /* static */ UniquePtr ReportingHeader::ParseHeader( nsIHttpChannel* aChannel, nsIURI* aURI, const nsACString& aHeaderValue) { MOZ_ASSERT(aURI); // aChannel can be null in gtest AutoJSAPI jsapi; JSObject* cleanGlobal = SimpleGlobalObject::Create(SimpleGlobalObject::GlobalType::BindingDetail); if (NS_WARN_IF(!cleanGlobal)) { return nullptr; } if (NS_WARN_IF(!jsapi.Init(cleanGlobal))) { return nullptr; } // WebIDL dictionary parses single items. Let's create a object to parse the // header. nsAutoString json; json.AppendASCII("{ \"items\": ["); json.Append(NS_ConvertUTF8toUTF16(aHeaderValue)); json.AppendASCII("]}"); JSContext* cx = jsapi.cx(); JS::Rooted jsonValue(cx); bool ok = JS_ParseJSON(cx, json.BeginReading(), json.Length(), &jsonValue); if (!ok) { LogToConsoleInvalidJSON(aChannel, aURI); return nullptr; } dom::ReportingHeaderValue data; if (!data.Init(cx, jsonValue)) { LogToConsoleInvalidJSON(aChannel, aURI); return nullptr; } if (!data.mItems.WasPassed() || data.mItems.Value().IsEmpty()) { return nullptr; } UniquePtr client = MakeUnique(); for (const dom::ReportingItem& item : data.mItems.Value()) { nsAutoString groupName; if (item.mGroup.isUndefined()) { groupName.AssignLiteral("default"); } else if (!item.mGroup.isString()) { LogToConsoleInvalidNameItem(aChannel, aURI); continue; } else { JS::Rooted groupStr(cx, item.mGroup.toString()); MOZ_ASSERT(groupStr); nsAutoJSString string; if (NS_WARN_IF(!string.init(cx, groupStr))) { continue; } groupName = string; } if (!item.mMax_age.isNumber() || !item.mEndpoints.isObject()) { LogToConsoleIncompleteItem(aChannel, aURI, groupName); continue; } JS::Rooted endpoints(cx, &item.mEndpoints.toObject()); MOZ_ASSERT(endpoints); bool isArray = false; if (!JS::IsArrayObject(cx, endpoints, &isArray) || !isArray) { LogToConsoleIncompleteItem(aChannel, aURI, groupName); continue; } uint32_t endpointsLength; if (!JS::GetArrayLength(cx, endpoints, &endpointsLength) || endpointsLength == 0) { LogToConsoleIncompleteItem(aChannel, aURI, groupName); continue; } const auto [begin, end] = client->mGroups.NonObservingRange(); if (std::any_of(begin, end, [&groupName](const Group& group) { return group.mName == groupName; })) { LogToConsoleDuplicateGroup(aChannel, aURI, groupName); continue; } Group* group = client->mGroups.AppendElement(); group->mName = groupName; group->mIncludeSubdomains = item.mInclude_subdomains; group->mTTL = item.mMax_age.toNumber(); group->mCreationTime = TimeStamp::Now(); for (uint32_t i = 0; i < endpointsLength; ++i) { JS::Rooted element(cx); if (!JS_GetElement(cx, endpoints, i, &element)) { return nullptr; } RootedDictionary endpoint(cx); if (!endpoint.Init(cx, element)) { LogToConsoleIncompleteEndpoint(aChannel, aURI, groupName); continue; } if (!endpoint.mUrl.isString() || (!endpoint.mPriority.isUndefined() && (!endpoint.mPriority.isNumber() || endpoint.mPriority.toNumber() < 0)) || (!endpoint.mWeight.isUndefined() && (!endpoint.mWeight.isNumber() || endpoint.mWeight.toNumber() < 0))) { LogToConsoleIncompleteEndpoint(aChannel, aURI, groupName); continue; } JS::Rooted endpointUrl(cx, endpoint.mUrl.toString()); MOZ_ASSERT(endpointUrl); nsAutoJSString endpointString; if (NS_WARN_IF(!endpointString.init(cx, endpointUrl))) { continue; } nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), endpointString); if (NS_FAILED(rv)) { LogToConsoleInvalidURLEndpoint(aChannel, aURI, groupName, endpointString); continue; } Endpoint* ep = group->mEndpoints.AppendElement(); ep->mUrl = uri; ep->mPriority = endpoint.mPriority.isUndefined() ? 1 : endpoint.mPriority.toNumber(); ep->mWeight = endpoint.mWeight.isUndefined() ? 1 : endpoint.mWeight.toNumber(); } } if (client->mGroups.IsEmpty()) { return nullptr; } return client; } bool ReportingHeader::IsSecureURI(nsIURI* aURI) const { MOZ_ASSERT(aURI); bool prioriAuthenticated = false; if (NS_WARN_IF(NS_FAILED(NS_URIChainHasFlags( aURI, nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY, &prioriAuthenticated)))) { return false; } return prioriAuthenticated; } /* static */ void ReportingHeader::LogToConsoleInvalidJSON(nsIHttpChannel* aChannel, nsIURI* aURI) { nsTArray params; LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidJSON", params); } /* static */ void ReportingHeader::LogToConsoleDuplicateGroup(nsIHttpChannel* aChannel, nsIURI* aURI, const nsAString& aName) { nsTArray params; params.AppendElement(aName); LogToConsoleInternal(aChannel, aURI, "ReportingHeaderDuplicateGroup", params); } /* static */ void ReportingHeader::LogToConsoleInvalidNameItem(nsIHttpChannel* aChannel, nsIURI* aURI) { nsTArray params; LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidNameItem", params); } /* static */ void ReportingHeader::LogToConsoleIncompleteItem(nsIHttpChannel* aChannel, nsIURI* aURI, const nsAString& aName) { nsTArray params; params.AppendElement(aName); LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidItem", params); } /* static */ void ReportingHeader::LogToConsoleIncompleteEndpoint(nsIHttpChannel* aChannel, nsIURI* aURI, const nsAString& aName) { nsTArray params; params.AppendElement(aName); LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidEndpoint", params); } /* static */ void ReportingHeader::LogToConsoleInvalidURLEndpoint(nsIHttpChannel* aChannel, nsIURI* aURI, const nsAString& aName, const nsAString& aURL) { nsTArray params; params.AppendElement(aURL); params.AppendElement(aName); LogToConsoleInternal(aChannel, aURI, "ReportingHeaderInvalidURLEndpoint", params); } /* static */ void ReportingHeader::LogToConsoleInternal(nsIHttpChannel* aChannel, nsIURI* aURI, const char* aMsg, const nsTArray& aParams) { MOZ_ASSERT(aURI); if (!aChannel) { // We are in a gtest. return; } uint64_t windowID = 0; nsresult rv = aChannel->GetTopLevelContentWindowId(&windowID); if (NS_WARN_IF(NS_FAILED(rv))) { return; } if (!windowID) { nsCOMPtr loadGroup; nsresult 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, "Reporting"_ns, windowID, aURI); Unused << NS_WARN_IF(NS_FAILED(rv)); } /* static */ void ReportingHeader::GetEndpointForReport( const nsAString& aGroupName, const mozilla::ipc::PrincipalInfo& aPrincipalInfo, nsACString& aEndpointURI) { auto principalOrErr = PrincipalInfoToPrincipal(aPrincipalInfo); if (NS_WARN_IF(principalOrErr.isErr())) { return; } nsCOMPtr principal = principalOrErr.unwrap(); GetEndpointForReport(aGroupName, principal, aEndpointURI); } /* static */ void ReportingHeader::GetEndpointForReport(const nsAString& aGroupName, nsIPrincipal* aPrincipal, nsACString& aEndpointURI) { MOZ_ASSERT(aEndpointURI.IsEmpty()); if (!gReporting) { return; } nsAutoCString origin; nsresult rv = aPrincipal->GetOrigin(origin); if (NS_WARN_IF(NS_FAILED(rv))) { return; } Client* client = gReporting->mOrigins.Get(origin); if (!client) { return; } const auto [begin, end] = client->mGroups.NonObservingRange(); const auto foundIt = std::find_if( begin, end, [&aGroupName](const Group& group) { return group.mName == aGroupName; }); if (foundIt != end) { GetEndpointForReportInternal(*foundIt, aEndpointURI); } // XXX More explicitly report an error if not found? } /* static */ void ReportingHeader::GetEndpointForReportInternal( const ReportingHeader::Group& aGroup, nsACString& aEndpointURI) { TimeDuration diff = TimeStamp::Now() - aGroup.mCreationTime; if (diff.ToSeconds() > aGroup.mTTL) { // Expired. return; } if (aGroup.mEndpoints.IsEmpty()) { return; } int64_t minPriority = -1; uint32_t totalWeight = 0; for (const Endpoint& endpoint : aGroup.mEndpoints.NonObservingRange()) { if (minPriority == -1 || minPriority > endpoint.mPriority) { minPriority = endpoint.mPriority; totalWeight = endpoint.mWeight; } else if (minPriority == endpoint.mPriority) { totalWeight += endpoint.mWeight; } } nsCOMPtr randomGenerator = do_GetService("@mozilla.org/security/random-generator;1"); if (NS_WARN_IF(!randomGenerator)) { return; } uint32_t randomNumber = 0; uint8_t* buffer; nsresult rv = randomGenerator->GenerateRandomBytes(sizeof(randomNumber), &buffer); if (NS_WARN_IF(NS_FAILED(rv))) { return; } memcpy(&randomNumber, buffer, sizeof(randomNumber)); free(buffer); totalWeight = randomNumber % totalWeight; const auto [begin, end] = aGroup.mEndpoints.NonObservingRange(); const auto foundIt = std::find_if( begin, end, [minPriority, totalWeight](const Endpoint& endpoint) { return minPriority == endpoint.mPriority && totalWeight < endpoint.mWeight; }); if (foundIt != end) { Unused << NS_WARN_IF(NS_FAILED(foundIt->mUrl->GetSpec(aEndpointURI))); } // XXX More explicitly report an error if not found? } /* static */ void ReportingHeader::RemoveEndpoint( const nsAString& aGroupName, const nsACString& aEndpointURL, const mozilla::ipc::PrincipalInfo& aPrincipalInfo) { if (!gReporting) { return; } nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aEndpointURL); if (NS_WARN_IF(NS_FAILED(rv))) { return; } auto principalOrErr = PrincipalInfoToPrincipal(aPrincipalInfo); if (NS_WARN_IF(principalOrErr.isErr())) { return; } nsAutoCString origin; rv = principalOrErr.unwrap()->GetOrigin(origin); if (NS_WARN_IF(NS_FAILED(rv))) { return; } Client* client = gReporting->mOrigins.Get(origin); if (!client) { return; } // Scope for the group iterator. { nsTObserverArray::BackwardIterator iter(client->mGroups); while (iter.HasMore()) { const Group& group = iter.GetNext(); if (group.mName != aGroupName) { continue; } // Scope for the endpoint iterator. { nsTObserverArray::BackwardIterator endpointIter( group.mEndpoints); while (endpointIter.HasMore()) { const Endpoint& endpoint = endpointIter.GetNext(); bool equal = false; rv = endpoint.mUrl->Equals(uri, &equal); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } if (equal) { endpointIter.Remove(); break; } } } if (group.mEndpoints.IsEmpty()) { iter.Remove(); } break; } } if (client->mGroups.IsEmpty()) { gReporting->mOrigins.Remove(origin); gReporting->MaybeCancelCleanupTimer(); } } void ReportingHeader::RemoveOriginsFromHost(const nsAString& aHost) { nsCOMPtr tldService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); if (NS_WARN_IF(!tldService)) { return; } NS_ConvertUTF16toUTF8 host(aHost); for (auto iter = mOrigins.Iter(); !iter.Done(); iter.Next()) { bool hasRootDomain = false; nsresult rv = tldService->HasRootDomain(iter.Key(), host, &hasRootDomain); if (NS_WARN_IF(NS_FAILED(rv)) || !hasRootDomain) { continue; } iter.Remove(); } MaybeCancelCleanupTimer(); } void ReportingHeader::RemoveOriginsFromOriginAttributesPattern( const OriginAttributesPattern& aPattern) { for (auto iter = mOrigins.Iter(); !iter.Done(); iter.Next()) { nsAutoCString suffix; OriginAttributes attr; if (NS_WARN_IF(!attr.PopulateFromOrigin(iter.Key(), suffix))) { continue; } if (aPattern.Matches(attr)) { iter.Remove(); } } MaybeCancelCleanupTimer(); } void ReportingHeader::RemoveOrigins() { mOrigins.Clear(); MaybeCancelCleanupTimer(); } void ReportingHeader::RemoveOriginsForTTL() { TimeStamp now = TimeStamp::Now(); for (auto iter = mOrigins.Iter(); !iter.Done(); iter.Next()) { Client* client = iter.UserData(); // Scope of the iterator. { nsTObserverArray::BackwardIterator groupIter(client->mGroups); while (groupIter.HasMore()) { const Group& group = groupIter.GetNext(); TimeDuration diff = now - group.mCreationTime; if (diff.ToSeconds() > group.mTTL) { groupIter.Remove(); return; } } } if (client->mGroups.IsEmpty()) { iter.Remove(); } } } /* static */ bool ReportingHeader::HasReportingHeaderForOrigin(const nsACString& aOrigin) { if (!gReporting) { return false; } return gReporting->mOrigins.Contains(aOrigin); } NS_IMETHODIMP ReportingHeader::Notify(nsITimer* aTimer) { mCleanupTimer = nullptr; RemoveOriginsForTTL(); MaybeCreateCleanupTimer(); return NS_OK; } NS_IMETHODIMP ReportingHeader::GetName(nsACString& aName) { aName.AssignLiteral("ReportingHeader"); return NS_OK; } void ReportingHeader::MaybeCreateCleanupTimer() { if (mCleanupTimer) { return; } if (mOrigins.Count() == 0) { return; } uint32_t timeout = StaticPrefs::dom_reporting_cleanup_timeout() * 1000; nsresult rv = NS_NewTimerWithCallback(getter_AddRefs(mCleanupTimer), this, timeout, nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY); Unused << NS_WARN_IF(NS_FAILED(rv)); } void ReportingHeader::MaybeCancelCleanupTimer() { if (!mCleanupTimer) { return; } if (mOrigins.Count() != 0) { return; } mCleanupTimer->Cancel(); mCleanupTimer = nullptr; } NS_INTERFACE_MAP_BEGIN(ReportingHeader) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY(nsITimerCallback) NS_INTERFACE_MAP_ENTRY(nsINamed) NS_INTERFACE_MAP_END NS_IMPL_ADDREF(ReportingHeader) NS_IMPL_RELEASE(ReportingHeader) } // namespace mozilla::dom