From c76a1ddc8dc4b2bccd3171dfa11cfbaad03d9504 Mon Sep 17 00:00:00 2001 From: David P Date: Fri, 8 Mar 2024 20:28:09 +0000 Subject: [PATCH] Bug 1879181: Allow skipping content analysis for requests that match url list r=gstoll,win-reviewers Check the URLs in the request against the prefs browser.contentanalysis.allow_url_regex_list and browser.contentanalysis.deny_url_regex_list, which are space-separated lists of ECMAscript regexs that match against ASCII-encoded URLs. Differential Revision: https://phabricator.services.mozilla.com/D203508 --- modules/libpref/init/StaticPrefList.yaml | 16 + .../contentanalysis/ContentAnalysis.cpp | 304 +++++++++++++----- .../contentanalysis/ContentAnalysis.h | 31 +- widget/windows/nsFilePicker.cpp | 1 + 4 files changed, 274 insertions(+), 78 deletions(-) diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index 634f09065738..01c3a329c289 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -1192,6 +1192,22 @@ value: "path_user" mirror: never +# Space-separated list of regexs that are compared to URLs of resources +# being checked by content-analysis. Resources that match are not checked +# and are always permitted. +- name: browser.contentanalysis.allow_url_regex_list + type: String + value: "" + mirror: never + +# Space-separated list of regexs that are compared to URLs of resources +# being checked by content-analysis. Resources that match are not checked +# and are always denied. +- name: browser.contentanalysis.deny_url_regex_list + type: String + value: "" + mirror: never + # Should CA ignore the system setting and use silent notifications? - name: browser.contentanalysis.silent_notifications type: bool diff --git a/toolkit/components/contentanalysis/ContentAnalysis.cpp b/toolkit/components/contentanalysis/ContentAnalysis.cpp index bf0c5be0eea6..f6821eb6d9b5 100644 --- a/toolkit/components/contentanalysis/ContentAnalysis.cpp +++ b/toolkit/components/contentanalysis/ContentAnalysis.cpp @@ -12,6 +12,7 @@ #include "GMPUtils.h" // ToHexString #include "mozilla/Components.h" #include "mozilla/dom/Promise.h" +#include "mozilla/dom/WindowGlobalParent.h" #include "mozilla/Logging.h" #include "mozilla/ScopeExit.h" #include "mozilla/Services.h" @@ -28,6 +29,7 @@ #include #include +#include #ifdef XP_WIN # include @@ -56,6 +58,8 @@ const char* kIsPerUserPref = "browser.contentanalysis.is_per_user"; const char* kPipePathNamePref = "browser.contentanalysis.pipe_path_name"; const char* kDefaultAllowPref = "browser.contentanalysis.default_allow"; const char* kClientSignature = "browser.contentanalysis.client_signature"; +const char* kAllowUrlPref = "browser.contentanalysis.allow_url_regex_list"; +const char* kDenyUrlPref = "browser.contentanalysis.deny_url_regex_list"; nsresult MakePromise(JSContext* aCx, RefPtr* aPromise) { nsIGlobalObject* go = xpc::CurrentNativeGlobal(aCx); @@ -480,8 +484,7 @@ static void LogRequest( } ContentAnalysisResponse::ContentAnalysisResponse( - content_analysis::sdk::ContentAnalysisResponse&& aResponse) - : mHasAcknowledged(false) { + content_analysis::sdk::ContentAnalysisResponse&& aResponse) { mAction = Action::eUnspecified; for (const auto& result : aResponse.results()) { if (!result.has_status() || @@ -509,7 +512,7 @@ ContentAnalysisResponse::ContentAnalysisResponse( ContentAnalysisResponse::ContentAnalysisResponse( Action aAction, const nsACString& aRequestToken) - : mAction(aAction), mRequestToken(aRequestToken), mHasAcknowledged(false) {} + : mAction(aAction), mRequestToken(aRequestToken) {} /* static */ already_AddRefed ContentAnalysisResponse::FromProtobuf( @@ -698,6 +701,122 @@ NS_IMETHODIMP ContentAnalysisResult::GetShouldAllowContent( return NS_OK; } +void ContentAnalysis::EnsureParsedUrlFilters() { + MOZ_ASSERT(NS_IsMainThread()); + if (mParsedUrlLists) { + return; + } + + mParsedUrlLists = true; + nsAutoCString allowList; + MOZ_ALWAYS_SUCCEEDS(Preferences::GetCString(kAllowUrlPref, allowList)); + for (const nsACString& regexSubstr : allowList.Split(u' ')) { + if (!regexSubstr.IsEmpty()) { + auto flatStr = PromiseFlatCString(regexSubstr); + const char* regex = flatStr.get(); + LOGD("CA will allow URLs that match %s", regex); + mAllowUrlList.push_back(std::regex(regex)); + } + } + + nsAutoCString denyList; + MOZ_ALWAYS_SUCCEEDS(Preferences::GetCString(kDenyUrlPref, denyList)); + for (const nsACString& regexSubstr : denyList.Split(u' ')) { + if (!regexSubstr.IsEmpty()) { + auto flatStr = PromiseFlatCString(regexSubstr); + const char* regex = flatStr.get(); + LOGD("CA will block URLs that match %s", regex); + mDenyUrlList.push_back(std::regex(regex)); + } + } +} + +ContentAnalysis::UrlFilterResult ContentAnalysis::FilterByUrlLists( + nsIContentAnalysisRequest* aRequest) { + EnsureParsedUrlFilters(); + + nsIURI* nsiUrl = nullptr; + MOZ_ALWAYS_SUCCEEDS(aRequest->GetUrl(&nsiUrl)); + nsCString urlString; + nsresult rv = nsiUrl->GetSpec(urlString); + NS_ENSURE_SUCCESS(rv, UrlFilterResult::eDeny); + MOZ_ASSERT(!urlString.IsEmpty()); + std::string url = urlString.BeginReading(); + size_t count = 0; + for (const auto& denyFilter : mDenyUrlList) { + if (std::regex_search(url, denyFilter)) { + LOGD("Denying CA request : Deny filter %zu matched url %s", count, + url.c_str()); + return UrlFilterResult::eDeny; + } + ++count; + } + + count = 0; + UrlFilterResult result = UrlFilterResult::eCheck; + for (const auto& allowFilter : mAllowUrlList) { + if (std::regex_match(url, allowFilter)) { + LOGD("CA request : Allow filter %zu matched %s", count, url.c_str()); + result = UrlFilterResult::eAllow; + break; + } + ++count; + } + + // The rest only applies to download resources. + nsIContentAnalysisRequest::AnalysisType analysisType; + MOZ_ALWAYS_SUCCEEDS(aRequest->GetAnalysisType(&analysisType)); + if (analysisType != ContentAnalysisRequest::AnalysisType::eFileDownloaded) { + MOZ_ASSERT(result == UrlFilterResult::eCheck || + result == UrlFilterResult::eAllow); + LOGD("CA request filter result: %s", + result == UrlFilterResult::eCheck ? "check" : "allow"); + return result; + } + + nsTArray> resources; + MOZ_ALWAYS_SUCCEEDS(aRequest->GetResources(resources)); + for (size_t resourceIdx = 0; resourceIdx < resources.Length(); + /* noop */) { + auto& resource = resources[resourceIdx]; + nsAutoString nsUrl; + MOZ_ALWAYS_SUCCEEDS(resource->GetUrl(nsUrl)); + std::string url = NS_ConvertUTF16toUTF8(nsUrl).get(); + count = 0; + for (auto& denyFilter : mDenyUrlList) { + if (std::regex_search(url, denyFilter)) { + LOGD( + "Denying CA request : Deny filter %zu matched download resource " + "at url %s", + count, url.c_str()); + return UrlFilterResult::eDeny; + } + ++count; + } + + count = 0; + bool removed = false; + for (auto& allowFilter : mAllowUrlList) { + if (std::regex_search(url, allowFilter)) { + LOGD( + "CA request : Allow filter %zu matched download resource " + "at url %s", + count, url.c_str()); + resources.RemoveElementAt(resourceIdx); + removed = true; + break; + } + ++count; + } + if (!removed) { + ++resourceIdx; + } + } + + // Check unless all were allowed. + return resources.Length() ? UrlFilterResult::eCheck : UrlFilterResult::eAllow; +} + NS_IMPL_CLASSINFO(ContentAnalysisRequest, nullptr, 0, {0}); NS_IMPL_ISUPPORTS_CI(ContentAnalysisRequest, nsIContentAnalysisRequest); NS_IMPL_CLASSINFO(ContentAnalysisResponse, nullptr, 0, {0}); @@ -866,6 +985,8 @@ nsresult ContentAnalysis::RunAnalyzeRequestTask( const RefPtr& aRequest, bool aAutoAcknowledge, int64_t aRequestCount, const RefPtr& aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + nsresult rv = NS_ERROR_FAILURE; auto callbackCopy = aCallback; auto se = MakeScopeExit([&] { @@ -875,24 +996,51 @@ nsresult ContentAnalysis::RunAnalyzeRequestTask( } }); - content_analysis::sdk::ContentAnalysisRequest pbRequest; - rv = - ConvertToProtobuf(aRequest, GetUserActionId(), aRequestCount, &pbRequest); + nsCString requestToken; + rv = aRequest->GetRequestToken(requestToken); NS_ENSURE_SUCCESS(rv, rv); - nsCString requestToken; nsMainThreadPtrHandle callbackHolderCopy( new nsMainThreadPtrHolder( "content analysis callback", aCallback)); CallbackData callbackData(std::move(callbackHolderCopy), aAutoAcknowledge); - rv = aRequest->GetRequestToken(requestToken); - NS_ENSURE_SUCCESS(rv, rv); { auto lock = mCallbackMap.Lock(); lock->InsertOrUpdate(requestToken, std::move(callbackData)); } + // Check URLs of requested info against + // browser.contentanalysis.allow_url_regex_list/deny_url_regex_list. + // Build the list once since creating regexs is slow. + // URLs that match the allow list are removed from the check. There is + // only one URL in all cases except downloads. If all contents are removed + // or the page URL is allowed (for downloads) then the operation is allowed. + // URLs that match the deny list block the entire operation. + // If the request is completely covered by this filter then flag it as + // not needing to send an Acknowledge. + auto filterResult = FilterByUrlLists(aRequest); + if (filterResult == ContentAnalysis::UrlFilterResult::eDeny) { + LOGD("Blocking request due to deny URL filter."); + auto response = ContentAnalysisResponse::FromAction( + nsIContentAnalysisResponse::Action::eBlock, requestToken); + response->DoNotAcknowledge(); + IssueResponse(response); + return NS_OK; + } else if (filterResult == ContentAnalysis::UrlFilterResult::eAllow) { + LOGD("Allowing request -- all operations match allow URL filter."); + auto response = ContentAnalysisResponse::FromAction( + nsIContentAnalysisResponse::Action::eAllow, requestToken); + response->DoNotAcknowledge(); + IssueResponse(response); + return NS_OK; + } + LOGD("Issuing ContentAnalysisRequest for token %s", requestToken.get()); + + content_analysis::sdk::ContentAnalysisRequest pbRequest; + rv = ConvertToProtobuf(aRequest, GetUserActionId(), aRequestCount, + &pbRequest); + NS_ENSURE_SUCCESS(rv, rv); LogRequest(&pbRequest); mCaClientPromise->Then( @@ -994,77 +1142,77 @@ void ContentAnalysis::DoAnalyzeRequest( LOGE("Content analysis got invalid response!"); return; } - nsCString responseRequestToken; - nsresult requestRv = response->GetRequestToken(responseRequestToken); - if (NS_FAILED(requestRv)) { - LOGE( - "Content analysis couldn't get request token " - "from response!"); - return; - } - Maybe maybeCallbackData; - { - auto callbackMap = owner->mCallbackMap.Lock(); - maybeCallbackData = callbackMap->Extract(responseRequestToken); - } - if (maybeCallbackData.isNothing()) { - LOGD( - "Content analysis did not find callback for " - "token %s", - responseRequestToken.get()); - return; - } - response->SetOwner(owner); - if (maybeCallbackData->Canceled()) { - // request has already been cancelled, so there's - // nothing to do - LOGD( - "Content analysis got response but ignoring " - "because it was already cancelled for token %s", - responseRequestToken.get()); - // Note that we always acknowledge here, even if - // autoAcknowledge isn't set, since we raise an exception - // at the caller on cancellation. - auto acknowledgement = MakeRefPtr( - nsIContentAnalysisAcknowledgement::Result::eTooLate, - nsIContentAnalysisAcknowledgement::FinalAction::eBlock); - response->Acknowledge(acknowledgement); - return; - } - - LOGD( - "Content analysis resolving response promise for " - "token %s", - responseRequestToken.get()); - nsIContentAnalysisResponse::Action action = response->GetAction(); - nsCOMPtr obsServ = - mozilla::services::GetObserverService(); - if (action == nsIContentAnalysisResponse::Action::eWarn) { - { - auto warnResponseDataMap = owner->mWarnResponseDataMap.Lock(); - warnResponseDataMap->InsertOrUpdate( - responseRequestToken, - WarnResponseData(std::move(*maybeCallbackData), response)); - } - obsServ->NotifyObservers(response, "dlp-response", nullptr); - return; - } - - obsServ->NotifyObservers(response, "dlp-response", nullptr); - if (maybeCallbackData->AutoAcknowledge()) { - auto acknowledgement = MakeRefPtr( - nsIContentAnalysisAcknowledgement::Result::eSuccess, - ConvertResult(action)); - response->Acknowledge(acknowledgement); - } - - nsMainThreadPtrHandle callbackHolder = - maybeCallbackData->TakeCallbackHolder(); - callbackHolder->ContentResult(response); + owner->IssueResponse(response); })); } +void ContentAnalysis::IssueResponse(RefPtr& response) { + MOZ_ASSERT(NS_IsMainThread()); + nsCString responseRequestToken; + nsresult requestRv = response->GetRequestToken(responseRequestToken); + if (NS_FAILED(requestRv)) { + LOGE("Content analysis couldn't get request token from response!"); + return; + } + + Maybe maybeCallbackData; + { + auto callbackMap = mCallbackMap.Lock(); + maybeCallbackData = callbackMap->Extract(responseRequestToken); + } + if (maybeCallbackData.isNothing()) { + LOGD("Content analysis did not find callback for token %s", + responseRequestToken.get()); + return; + } + response->SetOwner(this); + if (maybeCallbackData->Canceled()) { + // request has already been cancelled, so there's + // nothing to do + LOGD( + "Content analysis got response but ignoring " + "because it was already cancelled for token %s", + responseRequestToken.get()); + // Note that we always acknowledge here, even if + // autoAcknowledge isn't set, since we raise an exception + // at the caller on cancellation. + auto acknowledgement = MakeRefPtr( + nsIContentAnalysisAcknowledgement::Result::eTooLate, + nsIContentAnalysisAcknowledgement::FinalAction::eBlock); + response->Acknowledge(acknowledgement); + return; + } + + LOGD("Content analysis resolving response promise for token %s", + responseRequestToken.get()); + nsIContentAnalysisResponse::Action action = response->GetAction(); + nsCOMPtr obsServ = + mozilla::services::GetObserverService(); + if (action == nsIContentAnalysisResponse::Action::eWarn) { + { + auto warnResponseDataMap = mWarnResponseDataMap.Lock(); + warnResponseDataMap->InsertOrUpdate( + responseRequestToken, + WarnResponseData(std::move(*maybeCallbackData), response)); + } + obsServ->NotifyObservers(response, "dlp-response", nullptr); + return; + } + + obsServ->NotifyObservers(response, "dlp-response", nullptr); + if (maybeCallbackData->AutoAcknowledge()) { + auto acknowledgement = MakeRefPtr( + nsIContentAnalysisAcknowledgement::Result::eSuccess, + ConvertResult(action)); + response->Acknowledge(acknowledgement); + } + + nsMainThreadPtrHandle callbackHolder = + maybeCallbackData->TakeCallbackHolder(); + callbackHolder->ContentResult(response); +} + NS_IMETHODIMP ContentAnalysis::AnalyzeContentRequest(nsIContentAnalysisRequest* aRequest, bool aAutoAcknowledge, JSContext* aCx, @@ -1218,6 +1366,10 @@ ContentAnalysisResponse::Acknowledge( return NS_ERROR_FAILURE; } mHasAcknowledged = true; + + if (mDoNotAcknowledge) { + return NS_OK; + } return mOwner->RunAcknowledgeTask(aAcknowledgement, mRequestToken); }; diff --git a/toolkit/components/contentanalysis/ContentAnalysis.h b/toolkit/components/contentanalysis/ContentAnalysis.h index f4955ed452c2..afe87e251775 100644 --- a/toolkit/components/contentanalysis/ContentAnalysis.h +++ b/toolkit/components/contentanalysis/ContentAnalysis.h @@ -7,15 +7,25 @@ #define mozilla_contentanalysis_h #include "mozilla/DataMutex.h" -#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/MozPromise.h" +#include "mozilla/dom/Promise.h" #include "nsIContentAnalysis.h" #include "nsProxyRelease.h" #include "nsString.h" #include "nsTHashMap.h" #include +#include #include +class nsIPrincipal; +class ContentAnalysisTest; + +namespace mozilla::dom { +class DataTransfer; +class WindowGlobalParent; +} // namespace mozilla::dom + namespace content_analysis::sdk { class Client; class ContentAnalysisRequest; @@ -116,6 +126,14 @@ class ContentAnalysis final : public nsIContentAnalysis { nsCString aRequestToken, content_analysis::sdk::ContentAnalysisRequest&& aRequest, const std::shared_ptr& aClient); + void IssueResponse(RefPtr& response); + + // Did the URL filter completely handle the request or do we need to check + // with the agent. + enum UrlFilterResult { eCheck, eDeny, eAllow }; + + UrlFilterResult FilterByUrlLists(nsIContentAnalysisRequest* aRequest); + void EnsureParsedUrlFilters(); using ClientPromise = MozPromise, nsresult, @@ -158,6 +176,10 @@ class ContentAnalysis final : public nsIContentAnalysis { }; DataMutex> mWarnResponseDataMap; + std::vector mAllowUrlList; + std::vector mDenyUrlList; + bool mParsedUrlLists; + friend class ContentAnalysisResponse; }; @@ -172,6 +194,7 @@ class ContentAnalysisResponse final : public nsIContentAnalysisResponse { Action aAction, const nsACString& aRequestToken); void SetOwner(RefPtr aOwner); + void DoNotAcknowledge() { mDoNotAcknowledge = true; } private: ~ContentAnalysisResponse() = default; @@ -197,7 +220,11 @@ class ContentAnalysisResponse final : public nsIContentAnalysisResponse { RefPtr mOwner; // Whether the response has been acknowledged - bool mHasAcknowledged; + bool mHasAcknowledged = false; + + // If true, the request was completely handled by URL filter lists, so it + // was not sent to the agent and should not send an Acknowledge. + bool mDoNotAcknowledge = false; friend class ContentAnalysis; }; diff --git a/widget/windows/nsFilePicker.cpp b/widget/windows/nsFilePicker.cpp index e9365e675531..fb4c6e80b5a0 100644 --- a/widget/windows/nsFilePicker.cpp +++ b/widget/windows/nsFilePicker.cpp @@ -19,6 +19,7 @@ #include "mozilla/BackgroundHangMonitor.h" #include "mozilla/Components.h" #include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" #include "mozilla/dom/Directory.h" #include "mozilla/Logging.h" #include "mozilla/ipc/UtilityProcessManager.h"