1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/safe_browsing/client_side_detection_host.h"
6
7 #include <memory>
8 #include <utility>
9 #include <vector>
10
11 #include "base/bind.h"
12 #include "base/check_op.h"
13 #include "base/macros.h"
14 #include "base/memory/ptr_util.h"
15 #include "base/memory/ref_counted.h"
16 #include "base/metrics/histogram_functions.h"
17 #include "base/metrics/histogram_macros.h"
18 #include "base/sequenced_task_runner_helpers.h"
19 #include "base/strings/utf_string_conversions.h"
20 #include "base/time/default_tick_clock.h"
21 #include "base/time/tick_clock.h"
22 #include "chrome/browser/browser_process.h"
23 #include "chrome/browser/profiles/profile.h"
24 #include "chrome/browser/safe_browsing/client_side_detection_service.h"
25 #include "chrome/browser/safe_browsing/client_side_detection_service_factory.h"
26 #include "chrome/browser/safe_browsing/safe_browsing_service.h"
27 #include "chrome/browser/safe_browsing/user_interaction_observer.h"
28 #include "chrome/common/pref_names.h"
29 #include "components/prefs/pref_service.h"
30 #include "components/safe_browsing/content/common/safe_browsing.mojom-shared.h"
31 #include "components/safe_browsing/content/common/safe_browsing.mojom.h"
32 #include "components/safe_browsing/core/common/safe_browsing_prefs.h"
33 #include "components/safe_browsing/core/db/allowlist_checker_client.h"
34 #include "components/safe_browsing/core/db/database_manager.h"
35 #include "components/safe_browsing/core/proto/csd.pb.h"
36 #include "components/security_interstitials/content/unsafe_resource_util.h"
37 #include "content/public/browser/browser_task_traits.h"
38 #include "content/public/browser/browser_thread.h"
39 #include "content/public/browser/navigation_controller.h"
40 #include "content/public/browser/navigation_entry.h"
41 #include "content/public/browser/navigation_handle.h"
42 #include "content/public/browser/render_frame_host.h"
43 #include "content/public/browser/render_process_host.h"
44 #include "content/public/browser/web_contents.h"
45 #include "content/public/common/url_constants.h"
46 #include "net/base/ip_endpoint.h"
47 #include "net/http/http_response_headers.h"
48 #include "services/service_manager/public/cpp/interface_provider.h"
49 #include "third_party/blink/public/mojom/loader/referrer.mojom.h"
50 #include "url/gurl.h"
51
52 using content::BrowserThread;
53 using content::NavigationEntry;
54 using content::WebContents;
55
56 namespace safe_browsing {
57
58 typedef base::OnceCallback<void(bool)> ShouldClassifyUrlCallback;
59
60 // This class is instantiated each time a new toplevel URL loads, and
61 // asynchronously checks whether the phishing classifier should run
62 // for this URL. If so, it notifies the host class by calling the provided
63 // callback form the UI thread. Objects of this class are ref-counted and will
64 // be destroyed once nobody uses it anymore. If |web_contents|, |csd_service|
65 // or |host| go away you need to call Cancel(). We keep the |database_manager|
66 // alive in a ref pointer for as long as it takes.
67 class ClientSideDetectionHost::ShouldClassifyUrlRequest
68 : public base::RefCountedThreadSafe<
69 ClientSideDetectionHost::ShouldClassifyUrlRequest> {
70 public:
ShouldClassifyUrlRequest(content::NavigationHandle * navigation_handle,ShouldClassifyUrlCallback start_phishing_classification,WebContents * web_contents,ClientSideDetectionService * csd_service,SafeBrowsingDatabaseManager * database_manager,ClientSideDetectionHost * host)71 ShouldClassifyUrlRequest(
72 content::NavigationHandle* navigation_handle,
73 ShouldClassifyUrlCallback start_phishing_classification,
74 WebContents* web_contents,
75 ClientSideDetectionService* csd_service,
76 SafeBrowsingDatabaseManager* database_manager,
77 ClientSideDetectionHost* host)
78 : web_contents_(web_contents),
79 csd_service_(csd_service),
80 database_manager_(database_manager),
81 host_(host),
82 start_phishing_classification_cb_(
83 std::move(start_phishing_classification)) {
84 DCHECK_CURRENTLY_ON(BrowserThread::UI);
85 DCHECK(web_contents_);
86 DCHECK(csd_service_);
87 DCHECK(database_manager_.get());
88 DCHECK(host_);
89 url_ = navigation_handle->GetURL();
90 if (navigation_handle->GetResponseHeaders())
91 navigation_handle->GetResponseHeaders()->GetMimeType(&mime_type_);
92 remote_endpoint_ = navigation_handle->GetSocketAddress();
93 }
94
Start()95 void Start() {
96 DCHECK_CURRENTLY_ON(BrowserThread::UI);
97
98 // We start by doing some simple checks that can run on the UI thread.
99 base::UmaHistogramBoolean("SBClientPhishing.ClassificationStart", true);
100
101 // Only classify [X]HTML documents.
102 if (mime_type_ != "text/html" && mime_type_ != "application/xhtml+xml") {
103 DontClassifyForPhishing(NO_CLASSIFY_UNSUPPORTED_MIME_TYPE);
104 }
105
106 if (csd_service_->IsPrivateIPAddress(
107 remote_endpoint_.ToStringWithoutPort())) {
108 DontClassifyForPhishing(NO_CLASSIFY_PRIVATE_IP);
109 }
110
111 // For phishing we only classify HTTP or HTTPS pages.
112 if (!url_.SchemeIsHTTPOrHTTPS()) {
113 DontClassifyForPhishing(NO_CLASSIFY_SCHEME_NOT_SUPPORTED);
114 }
115
116 // Don't run any classifier if the tab is incognito.
117 if (web_contents_->GetBrowserContext()->IsOffTheRecord()) {
118 DontClassifyForPhishing(NO_CLASSIFY_OFF_THE_RECORD);
119 }
120
121 // Don't start classification if |url_| is whitelisted by enterprise policy.
122 Profile* profile =
123 Profile::FromBrowserContext(web_contents_->GetBrowserContext());
124 if (profile && IsURLWhitelistedByPolicy(url_, *profile->GetPrefs())) {
125 DontClassifyForPhishing(NO_CLASSIFY_WHITELISTED_BY_POLICY);
126 }
127
128 // If the tab has a delayed warning, ignore this second verdict. We don't
129 // want to immediately undelay a page that's already blocked as phishy.
130 if (SafeBrowsingUserInteractionObserver::FromWebContents(web_contents_)) {
131 DontClassifyForPhishing(NO_CLASSIFY_HAS_DELAYED_WARNING);
132 }
133
134 // We lookup the csd-whitelist before we lookup the cache because
135 // a URL may have recently been whitelisted. If the URL matches
136 // the csd-whitelist we won't start phishing classification. The
137 // csd-whitelist check has to be done on the IO thread because it
138 // uses the SafeBrowsing service class.
139 if (ShouldClassifyForPhishing()) {
140 content::GetIOThreadTaskRunner({})->PostTask(
141 FROM_HERE,
142 base::BindOnce(&ShouldClassifyUrlRequest::CheckSafeBrowsingDatabase,
143 this, url_));
144 }
145 }
146
Cancel()147 void Cancel() {
148 DontClassifyForPhishing(NO_CLASSIFY_CANCEL);
149 // Just to make sure we don't do anything stupid we reset all these
150 // pointers except for the safebrowsing service class which may be
151 // accessed by CheckSafeBrowsingDatabase().
152 web_contents_ = nullptr;
153 csd_service_ = nullptr;
154 host_ = nullptr;
155 }
156
157 private:
158 friend class base::RefCountedThreadSafe<
159 ClientSideDetectionHost::ShouldClassifyUrlRequest>;
160
161 // Enum used to keep stats about why the pre-classification check failed.
162 enum PreClassificationCheckResult {
163 OBSOLETE_NO_CLASSIFY_PROXY_FETCH = 0,
164 NO_CLASSIFY_PRIVATE_IP = 1,
165 NO_CLASSIFY_OFF_THE_RECORD = 2,
166 NO_CLASSIFY_MATCH_CSD_WHITELIST = 3,
167 NO_CLASSIFY_TOO_MANY_REPORTS = 4,
168 NO_CLASSIFY_UNSUPPORTED_MIME_TYPE = 5,
169 NO_CLASSIFY_NO_DATABASE_MANAGER = 6,
170 NO_CLASSIFY_KILLSWITCH = 7,
171 NO_CLASSIFY_CANCEL = 8,
172 NO_CLASSIFY_RESULT_FROM_CACHE = 9,
173 DEPRECATED_NO_CLASSIFY_NOT_HTTP_URL = 10,
174 NO_CLASSIFY_SCHEME_NOT_SUPPORTED = 11,
175 NO_CLASSIFY_WHITELISTED_BY_POLICY = 12,
176 CLASSIFY = 13,
177 NO_CLASSIFY_HAS_DELAYED_WARNING = 14,
178
179 NO_CLASSIFY_MAX // Always add new values before this one.
180 };
181
182 // The destructor can be called either from the UI or the IO thread.
~ShouldClassifyUrlRequest()183 virtual ~ShouldClassifyUrlRequest() { }
184
ShouldClassifyForPhishing() const185 bool ShouldClassifyForPhishing() const {
186 DCHECK_CURRENTLY_ON(BrowserThread::UI);
187 return !start_phishing_classification_cb_.is_null();
188 }
189
DontClassifyForPhishing(PreClassificationCheckResult reason)190 void DontClassifyForPhishing(PreClassificationCheckResult reason) {
191 DCHECK_CURRENTLY_ON(BrowserThread::UI);
192 if (ShouldClassifyForPhishing()) {
193 // Track the first reason why we stopped classifying for phishing.
194 base::UmaHistogramEnumeration(
195 "SBClientPhishing.PreClassificationCheckResult", reason,
196 NO_CLASSIFY_MAX);
197 std::move(start_phishing_classification_cb_).Run(false);
198 }
199 start_phishing_classification_cb_.Reset();
200 }
201
CheckSafeBrowsingDatabase(const GURL & url)202 void CheckSafeBrowsingDatabase(const GURL& url) {
203 DCHECK_CURRENTLY_ON(BrowserThread::IO);
204 PreClassificationCheckResult phishing_reason = NO_CLASSIFY_MAX;
205 if (!database_manager_.get()) {
206 // We cannot check the Safe Browsing whitelists so we stop here
207 // for safety.
208 OnWhitelistCheckDoneOnIO(url, NO_CLASSIFY_NO_DATABASE_MANAGER,
209 /*match_whitelist=*/false);
210 return;
211 }
212
213 // Query the CSD Whitelist asynchronously. We're already on the IO thread so
214 // can call AllowlistCheckerClient directly.
215 base::OnceCallback<void(bool)> result_callback =
216 base::BindOnce(&ClientSideDetectionHost::ShouldClassifyUrlRequest::
217 OnWhitelistCheckDoneOnIO,
218 this, url, phishing_reason);
219 AllowlistCheckerClient::StartCheckCsdWhitelist(database_manager_, url,
220 std::move(result_callback));
221 }
222
OnWhitelistCheckDoneOnIO(const GURL & url,PreClassificationCheckResult phishing_reason,bool match_whitelist)223 void OnWhitelistCheckDoneOnIO(const GURL& url,
224 PreClassificationCheckResult phishing_reason,
225 bool match_whitelist) {
226 DCHECK_CURRENTLY_ON(BrowserThread::IO);
227 // We don't want to call the classification callbacks from the IO
228 // thread so we simply pass the results of this method to CheckCache()
229 // which is called on the UI thread;
230 if (match_whitelist) {
231 phishing_reason = NO_CLASSIFY_MATCH_CSD_WHITELIST;
232 }
233 content::GetUIThreadTaskRunner({})->PostTask(
234 FROM_HERE, base::BindOnce(&ShouldClassifyUrlRequest::CheckCache, this,
235 phishing_reason));
236 }
237
CheckCache(PreClassificationCheckResult phishing_reason)238 void CheckCache(PreClassificationCheckResult phishing_reason) {
239 DCHECK_CURRENTLY_ON(BrowserThread::UI);
240 if (phishing_reason != NO_CLASSIFY_MAX)
241 DontClassifyForPhishing(phishing_reason);
242 if (!ShouldClassifyForPhishing()) {
243 return; // No point in doing anything else.
244 }
245 // If result is cached, we don't want to run classification again.
246 // In that case we're just trying to show the warning.
247 bool is_phishing;
248 if (csd_service_->GetValidCachedResult(url_, &is_phishing)) {
249 base::UmaHistogramBoolean("SBClientPhishing.RequestSatisfiedFromCache",
250 true);
251 // Since we are already on the UI thread, this is safe.
252 host_->MaybeShowPhishingWarning(/*is_from_cache=*/true, url_,
253 is_phishing);
254 DontClassifyForPhishing(NO_CLASSIFY_RESULT_FROM_CACHE);
255 }
256
257 // We want to limit the number of requests, though we will ignore the
258 // limit for urls in the cache. We don't want to start classifying
259 // too many pages as phishing, but for those that we already think are
260 // phishing we want to send a request to the server to give ourselves
261 // a chance to fix misclassifications.
262 if (csd_service_->IsInCache(url_)) {
263 base::UmaHistogramBoolean("SBClientPhishing.ReportLimitSkipped", true);
264 } else if (csd_service_->OverPhishingReportLimit()) {
265 DontClassifyForPhishing(NO_CLASSIFY_TOO_MANY_REPORTS);
266 }
267
268 // Everything checks out, so start classification.
269 // |web_contents_| is safe to call as we will be destructed
270 // before it is.
271 if (ShouldClassifyForPhishing()) {
272 base::UmaHistogramEnumeration(
273 "SBClientPhishing.PreClassificationCheckResult", CLASSIFY,
274 NO_CLASSIFY_MAX);
275 std::move(start_phishing_classification_cb_).Run(true);
276 // Reset the callback to make sure ShouldClassifyForPhishing()
277 // returns false.
278 start_phishing_classification_cb_.Reset();
279 }
280 }
281
282 GURL url_;
283 std::string mime_type_;
284 net::IPEndPoint remote_endpoint_;
285 WebContents* web_contents_;
286 ClientSideDetectionService* csd_service_;
287 // We keep a ref pointer here just to make sure the safe browsing
288 // database manager stays alive long enough.
289 scoped_refptr<SafeBrowsingDatabaseManager> database_manager_;
290 ClientSideDetectionHost* host_;
291
292 ShouldClassifyUrlCallback start_phishing_classification_cb_;
293
294 DISALLOW_COPY_AND_ASSIGN(ShouldClassifyUrlRequest);
295 };
296
297 // static
Create(WebContents * tab)298 std::unique_ptr<ClientSideDetectionHost> ClientSideDetectionHost::Create(
299 WebContents* tab) {
300 return base::WrapUnique(new ClientSideDetectionHost(tab));
301 }
302
ClientSideDetectionHost(WebContents * tab)303 ClientSideDetectionHost::ClientSideDetectionHost(WebContents* tab)
304 : content::WebContentsObserver(tab),
305 csd_service_(nullptr),
306 tab_(tab),
307 classification_request_(nullptr),
308 pageload_complete_(false),
309 tick_clock_(base::DefaultTickClock::GetInstance()) {
310 DCHECK(tab);
311 // Note: csd_service_ and sb_service will be nullptr here in testing.
312 csd_service_ = ClientSideDetectionServiceFactory::GetForProfile(
313 Profile::FromBrowserContext(tab->GetBrowserContext()));
314
315 scoped_refptr<SafeBrowsingService> sb_service =
316 g_browser_process->safe_browsing_service();
317 if (sb_service.get()) {
318 ui_manager_ = sb_service->ui_manager();
319 database_manager_ = sb_service->database_manager();
320 }
321 }
322
~ClientSideDetectionHost()323 ClientSideDetectionHost::~ClientSideDetectionHost() {
324 if (csd_service_)
325 csd_service_->RemoveClientSideDetectionHost(this);
326 }
327
DidFinishNavigation(content::NavigationHandle * navigation_handle)328 void ClientSideDetectionHost::DidFinishNavigation(
329 content::NavigationHandle* navigation_handle) {
330 if (!navigation_handle->IsInMainFrame() || !navigation_handle->HasCommitted())
331 return;
332
333 // TODO(noelutz): move this DCHECK to WebContents and fix all the unit tests
334 // that don't call this method on the UI thread.
335 // DCHECK_CURRENTLY_ON(BrowserThread::UI);
336 if (navigation_handle->IsSameDocument()) {
337 // If the navigation is within the same document, the user isn't really
338 // navigating away. We don't need to cancel a pending callback or
339 // begin a new classification.
340 return;
341 }
342 // Cancel any pending classification request.
343 if (classification_request_.get()) {
344 classification_request_->Cancel();
345 }
346 // If we navigate away and there currently is a pending phishing
347 // report request we have to cancel it to make sure we don't display
348 // an interstitial for the wrong page. Note that this won't cancel
349 // the server ping back but only cancel the showing of the
350 // interstitial.
351 weak_factory_.InvalidateWeakPtrs();
352
353 if (!csd_service_) {
354 return;
355 }
356
357 current_url_ = navigation_handle->GetURL();
358
359 pageload_complete_ = false;
360
361 // Check whether we can cassify the current URL for phishing.
362 classification_request_ = new ShouldClassifyUrlRequest(
363 navigation_handle,
364 base::BindOnce(&ClientSideDetectionHost::OnPhishingPreClassificationDone,
365 weak_factory_.GetWeakPtr()),
366 web_contents(), csd_service_, database_manager_.get(), this);
367 classification_request_->Start();
368 }
369
SendModelToRenderFrame()370 void ClientSideDetectionHost::SendModelToRenderFrame() {
371 DCHECK_CURRENTLY_ON(BrowserThread::UI);
372 if (!web_contents() || web_contents() != tab_ || !csd_service_)
373 return;
374
375 for (content::RenderFrameHost* frame : web_contents()->GetAllFrames()) {
376 if (phishing_detector_)
377 phishing_detector_.reset();
378 frame->GetRemoteInterfaces()->GetInterface(
379 phishing_detector_.BindNewPipeAndPassReceiver());
380 phishing_detector_->SetPhishingModel(csd_service_->GetModelStr());
381 }
382 }
383
WebContentsDestroyed()384 void ClientSideDetectionHost::WebContentsDestroyed() {
385 // Tell any pending classification request that it is being canceled.
386 if (classification_request_.get()) {
387 classification_request_->Cancel();
388 }
389 if (csd_service_)
390 csd_service_->RemoveClientSideDetectionHost(this);
391 }
392
RenderFrameCreated(content::RenderFrameHost * render_frame_host)393 void ClientSideDetectionHost::RenderFrameCreated(
394 content::RenderFrameHost* render_frame_host) {
395 if (phishing_detector_)
396 phishing_detector_.reset();
397 render_frame_host->GetRemoteInterfaces()->GetInterface(
398 phishing_detector_.BindNewPipeAndPassReceiver());
399 phishing_detector_->SetPhishingModel(csd_service_->GetModelStr());
400 }
401
OnPhishingPreClassificationDone(bool should_classify)402 void ClientSideDetectionHost::OnPhishingPreClassificationDone(
403 bool should_classify) {
404 DCHECK_CURRENTLY_ON(BrowserThread::UI);
405 if (should_classify) {
406 content::RenderFrameHost* rfh = web_contents()->GetMainFrame();
407 phishing_detector_.reset();
408 rfh->GetRemoteInterfaces()->GetInterface(
409 phishing_detector_.BindNewPipeAndPassReceiver());
410 phishing_detection_start_time_ = tick_clock_->NowTicks();
411 phishing_detector_->StartPhishingDetection(
412 current_url_,
413 base::BindOnce(&ClientSideDetectionHost::PhishingDetectionDone,
414 weak_factory_.GetWeakPtr()));
415 }
416 }
417
PhishingDetectionDone(mojom::PhishingDetectorResult result,const std::string & verdict_str)418 void ClientSideDetectionHost::PhishingDetectionDone(
419 mojom::PhishingDetectorResult result,
420 const std::string& verdict_str) {
421 DCHECK_CURRENTLY_ON(BrowserThread::UI);
422 // There is something seriously wrong if there is no service class but
423 // this method is called. The renderer should not start phishing detection
424 // if there isn't any service class in the browser.
425 DCHECK(csd_service_);
426
427 UmaHistogramMediumTimes(
428 "SBClientPhishing.PhishingDetectionDuration",
429 base::TimeTicks::Now() - phishing_detection_start_time_);
430 base::UmaHistogramEnumeration("SBClientPhishing.PhishingDetectorResult",
431 result);
432 if (result == mojom::PhishingDetectorResult::CLASSIFIER_NOT_READY) {
433 base::UmaHistogramEnumeration("SBClientPhishing.ClassifierNotReadyReason",
434 csd_service_->GetLastModelStatus());
435 }
436 if (result != mojom::PhishingDetectorResult::SUCCESS)
437 return;
438
439 // We parse the protocol buffer here. If we're unable to parse it we won't
440 // send the verdict further.
441 std::unique_ptr<ClientPhishingRequest> verdict(new ClientPhishingRequest);
442 if (csd_service_ &&
443 verdict->ParseFromString(verdict_str) &&
444 verdict->IsInitialized()) {
445 VLOG(2) << "Phishing classification score: " << verdict->client_score();
446 Profile* profile =
447 Profile::FromBrowserContext(web_contents()->GetBrowserContext());
448 if (!IsExtendedReportingEnabled(*profile->GetPrefs()) &&
449 !IsEnhancedProtectionEnabled(*profile->GetPrefs())) {
450 // These fields should only be set for SBER users.
451 verdict->clear_screenshot_digest();
452 verdict->clear_screenshot_phash();
453 verdict->clear_phash_dimension_size();
454 }
455
456 base::UmaHistogramBoolean("SBClientPhishing.LocalModelDetectsPhishing",
457 verdict->is_phishing());
458
459 // We only send phishing verdict to the server if the verdict is phishing.
460 if (verdict->is_phishing()) {
461 ClientSideDetectionService::ClientReportPhishingRequestCallback callback =
462 base::BindOnce(&ClientSideDetectionHost::MaybeShowPhishingWarning,
463 weak_factory_.GetWeakPtr(),
464 /*is_from_cache=*/false);
465 Profile* profile =
466 Profile::FromBrowserContext(web_contents()->GetBrowserContext());
467 csd_service_->SendClientReportPhishingRequest(
468 std::move(verdict), IsExtendedReportingEnabled(*profile->GetPrefs()),
469 IsEnhancedProtectionEnabled(*profile->GetPrefs()),
470 std::move(callback));
471 }
472 }
473 }
474
MaybeShowPhishingWarning(bool is_from_cache,GURL phishing_url,bool is_phishing)475 void ClientSideDetectionHost::MaybeShowPhishingWarning(bool is_from_cache,
476 GURL phishing_url,
477 bool is_phishing) {
478 DCHECK_CURRENTLY_ON(BrowserThread::UI);
479 if (is_from_cache) {
480 base::UmaHistogramBoolean("SBClientPhishing.CacheDetectsPhishing",
481 is_phishing);
482 } else {
483 base::UmaHistogramBoolean("SBClientPhishing.ServerModelDetectsPhishing",
484 is_phishing);
485 }
486
487 if (is_phishing) {
488 DCHECK(web_contents());
489 if (ui_manager_.get()) {
490 security_interstitials::UnsafeResource resource;
491 resource.url = phishing_url;
492 resource.original_url = phishing_url;
493 resource.is_subresource = false;
494 resource.threat_type = SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING;
495 resource.threat_source =
496 safe_browsing::ThreatSource::CLIENT_SIDE_DETECTION;
497 resource.web_contents_getter =
498 security_interstitials::GetWebContentsGetter(
499 web_contents()->GetMainFrame()->GetProcess()->GetID(),
500 web_contents()->GetMainFrame()->GetRoutingID());
501 if (!ui_manager_->IsWhitelisted(resource)) {
502 // We need to stop any pending navigations, otherwise the interstitial
503 // might not get created properly.
504 web_contents()->GetController().DiscardNonCommittedEntries();
505 }
506 ui_manager_->DisplayBlockingPage(resource);
507 }
508 // If there is true phishing verdict, invalidate weakptr so that no longer
509 // consider the malware vedict.
510 weak_factory_.InvalidateWeakPtrs();
511 }
512 }
513
set_client_side_detection_service(ClientSideDetectionService * service)514 void ClientSideDetectionHost::set_client_side_detection_service(
515 ClientSideDetectionService* service) {
516 csd_service_ = service;
517 }
518
set_ui_manager(SafeBrowsingUIManager * ui_manager)519 void ClientSideDetectionHost::set_ui_manager(
520 SafeBrowsingUIManager* ui_manager) {
521 ui_manager_ = ui_manager;
522 }
523
set_database_manager(SafeBrowsingDatabaseManager * database_manager)524 void ClientSideDetectionHost::set_database_manager(
525 SafeBrowsingDatabaseManager* database_manager) {
526 database_manager_ = database_manager;
527 }
528
529 } // namespace safe_browsing
530