-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add persistent storage for ignored phishing sites #17463
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,6 +18,7 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { CommandDefinition, MessageListener } from "@bitwarden/messaging"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { GlobalStateProvider, KeyDefinition, PHISHING_DETECTION_DISK } from "@bitwarden/state"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { BrowserApi } from "../../../platform/browser/browser-api"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -44,6 +45,17 @@ export const PHISHING_DETECTION_CANCEL_COMMAND = new CommandDefinition<{ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tabId: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }>("phishing-detection-cancel"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Key definition for storing hostnames that the user has chosen to ignore | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const IGNORED_PHISHING_HOSTNAMES_KEY = new KeyDefinition<string[]>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PHISHING_DETECTION_DISK, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "ignoredPhishingHostnames", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deserializer: (value: string[]) => value ?? [], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+48
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ Missing documentation: Please add a comment explaining:
Suggested change
Comment on lines
+51
to
+57
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion(non-blocking): you can also use the array helper
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export class PhishingDetectionService { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static _tabUpdated$ = new Subject<PhishingDetectionNavigationEvent>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static _ignoredHostnames = new Set<string>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -56,6 +68,7 @@ export class PhishingDetectionService { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logService: LogService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| phishingDataService: PhishingDataService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messageListener: MessageListener, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| globalStateProvider: GlobalStateProvider, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (this._didInit) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logService.debug("[PhishingDetectionService] Initialize already called. Aborting."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -64,16 +77,44 @@ export class PhishingDetectionService { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logService.debug("[PhishingDetectionService] Initialize called. Checking prerequisites..."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ignoredHostnamesState = globalStateProvider.get(IGNORED_PHISHING_HOSTNAMES_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Subscribe to state changes (including initial state) to keep in-memory Set in sync | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // This handles both the initial load and any future updates from other contexts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ignoredHostnamesSub = ignoredHostnamesState.state$ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .pipe( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| map((hostnames) => hostnames ?? []), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tap((hostnames) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this._ignoredHostnames = new Set(hostnames); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: don't eject data from observable flows, it risks causing race-conditions. Just subscribe to it directly where it's needed example: const onContinueCommand$ = combineLatest([messageListener.messages$(PHISHING_DETECTION_CONTINUE_COMMAND), ignoredHostnamesState.state$]).pipe(
tap(([message, ignoredHostnames]) =>
logService.debug(`[PhishingDetectionService] user selected continue for ${message.url}`),
), |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logService.debug( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `[PhishingDetectionService] Loaded/updated ${hostnames.length} ignored hostnames`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .subscribe(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BrowserApi.addListener(chrome.tabs.onUpdated, this._handleTabUpdated.bind(this)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const onContinueCommand$ = messageListener.messages$(PHISHING_DETECTION_CONTINUE_COMMAND).pipe( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tap((message) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logService.debug(`[PhishingDetectionService] user selected continue for ${message.url}`), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| concatMap(async (message) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = new URL(message.url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this._ignoredHostnames.add(url.hostname); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await BrowserApi.navigateTabToUrl(message.tabId, url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = new URL(message.url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this._ignoredHostnames.add(url.hostname); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Persist to storage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await ignoredHostnamesState.update(() => Array.from(this._ignoredHostnames)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+105
to
+107
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: this is susceptible to race-conditions because awaiting Either stash a copy of the |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logService.debug( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `[PhishingDetectionService] Added ${url.hostname} to ignored hostnames (persisted)`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await BrowserApi.navigateTabToUrl(message.tabId, url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logService.error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `[PhishingDetectionService] Failed to process continue command for URL: ${message.url}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -97,8 +138,10 @@ export class PhishingDetectionService { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tap((event) => logService.debug(`[PhishingDetectionService] processing event:`, event)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| concatMap(async ({ tabId, url, ignored }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (ignored) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The next time this host is visited, block again | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this._ignoredHostnames.delete(url.hostname); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // User has previously chosen to continue to this hostname, skip phishing check | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logService.debug( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `[PhishingDetectionService] Skipping phishing check for ignored hostname: ${url.hostname}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isPhishing = await phishingDataService.isPhishingDomain(url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -116,7 +159,7 @@ export class PhishingDetectionService { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const onCancelCommand$ = messageListener | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .messages$(PHISHING_DETECTION_CANCEL_COMMAND) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .pipe(switchMap((message) => BrowserApi.closeTab(message.tabId))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .pipe(concatMap(async (message) => await BrowserApi.closeTab(message.tabId))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const activeAccountHasAccess$ = combineLatest([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| accountService.activeAccount$, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -158,6 +201,7 @@ export class PhishingDetectionService { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this._didInit = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| initSub.unsubscribe(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ignoredHostnamesSub.unsubscribe(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this._didInit = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Manually type cast to satisfy the listener signature due to the mixture | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
โ Insufficient test coverage (30.43%): This test file only verifies that initialization doesn't throw. Critical functionality is completely untested:
Please add tests covering the main user flows and error scenarios.