Skip to content

Conversation

@maxkpower
Copy link
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-28394

📔 Objective

  • Add persistent storage for ignored phishing sites so users don't see the blocker again after clicking "Continue to this site (not recommended)"
  • Store ignored hostnames in disk storage using GlobalStateProvider
  • Load ignored hostnames on service initialization to avoid race conditions

📸 Screenshots

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@github-actions
Copy link
Contributor

github-actions bot commented Nov 18, 2025

Logo
Checkmarx One – Scan Summary & Details536bdbc6-89cd-4beb-b46b-0b5287543f82

New Issues (1)

Checkmarx found the following issues in this Pull Request

Severity Issue Source File / Package Checkmarx Insight
MEDIUM Missing_HSTS_Header /apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts: 143
detailsThe web-application does not define an HSTS header, leaving it vulnerable to attack.
ID: alhjfubOVRYrjEwL%2BQtMJ8mzlvY%3D
Attack Vector

@codecov
Copy link

codecov bot commented Nov 19, 2025

Codecov Report

❌ Patch coverage is 30.43478% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 41.11%. Comparing base (413a024) to head (836fb50).
⚠️ Report is 26 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...g-detection/services/phishing-detection.service.ts 35.00% 11 Missing and 2 partials ⚠️
apps/browser/src/platform/browser/browser-api.ts 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #17463      +/-   ##
==========================================
- Coverage   41.11%   41.11%   -0.01%     
==========================================
  Files        3544     3543       -1     
  Lines      101753   101859     +106     
  Branches    15241    15265      +24     
==========================================
+ Hits        41836    41878      +42     
- Misses      58157    58215      +58     
- Partials     1760     1766       +6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@maxkpower maxkpower marked this pull request as ready for review November 20, 2025 15:30
@maxkpower maxkpower requested review from a team as code owners November 20, 2025 15:30
@maxkpower maxkpower requested a review from coroiu November 20, 2025 15:30
@claude
Copy link

claude bot commented Nov 20, 2025

Claude Code is working…

I'll analyze this and get back to you.

View job run

Comment on lines 81 to 92
// Load previously ignored hostnames from storage (synchronously to avoid race conditions)
const ignoredHostnamesState = globalStateProvider.get(IGNORED_PHISHING_HOSTNAMES_KEY);
firstValueFrom(ignoredHostnamesState.state$.pipe(map((hostnames) => hostnames ?? [])))
.then((initialHostnames) => {
this._ignoredHostnames = new Set(initialHostnames);
logService.debug(
`[PhishingDetectionService] Loaded ${initialHostnames.length} ignored hostnames from storage`,
);
})
.catch((error) => {
logService.error("[PhishingDetectionService] Failed to load ignored hostnames", error);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: The initial state load is not awaited, which means the service may start processing navigation events before _ignoredHostnames is populated from disk. This defeats the purpose of persistent storage.

If a user navigates to a previously-ignored phishing site immediately after browser startup, they'll see the warning again.

Suggested change
// Load previously ignored hostnames from storage (synchronously to avoid race conditions)
const ignoredHostnamesState = globalStateProvider.get(IGNORED_PHISHING_HOSTNAMES_KEY);
firstValueFrom(ignoredHostnamesState.state$.pipe(map((hostnames) => hostnames ?? [])))
.then((initialHostnames) => {
this._ignoredHostnames = new Set(initialHostnames);
logService.debug(
`[PhishingDetectionService] Loaded ${initialHostnames.length} ignored hostnames from storage`,
);
})
.catch((error) => {
logService.error("[PhishingDetectionService] Failed to load ignored hostnames", error);
});
// Load previously ignored hostnames from storage (await to avoid race conditions)
const ignoredHostnamesState = globalStateProvider.get(IGNORED_PHISHING_HOSTNAMES_KEY);
try {
const initialHostnames = await firstValueFrom(
ignoredHostnamesState.state$.pipe(map((hostnames) => hostnames ?? []))
);
this._ignoredHostnames = new Set(initialHostnames);
logService.debug(
`[PhishingDetectionService] Loaded ${initialHostnames.length} ignored hostnames from storage`,
);
} catch (error) {
logService.error("[PhishingDetectionService] Failed to load ignored hostnames", error);
}

globalStateProvider,
);
}).not.toThrow();
});
Copy link

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:

  • Loading ignored hostnames from storage on init
  • Persisting hostnames when user clicks "Continue"
  • Navigation to ignored sites (should skip phishing check)
  • Error handling during storage operations
  • Cleanup/unsubscribe behavior

Please add tests covering the main user flows and error scenarios.

Comment on lines +49 to +58
/**
* 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 ?? [],
},
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Missing documentation: Please add a comment explaining:

  • This is global state (shared across all accounts)
  • Data persists across browser restarts
  • Not synced across devices
Suggested change
/**
* 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 ?? [],
},
);
/**
* Key definition for storing hostnames that the user has chosen to ignore.
* This is global state (shared across all accounts) stored in disk storage.
* Data persists across browser restarts but is not synced across devices.
*/
export const IGNORED_PHISHING_HOSTNAMES_KEY = new KeyDefinition<string[]>(
PHISHING_DETECTION_DISK,
"ignoredPhishingHostnames",
{
deserializer: (value: string[]) => value ?? [],
},
);

@bw-ghapp
Copy link
Contributor

bw-ghapp bot commented Nov 20, 2025

⚠️ Changes in this PR impact the Autofill experience of the browser client ⚠️

BIT has tested the core experience with these changes and the feature flag configuration used by vault.bitwarden.com

Caution

Unfortunately, one or more of these tests failed. 😞

Please resolve the failure before merging; reach out to @bitwarden/team-autofill-dev if you'd like help.

You can view the detailed results of the tests here.

@bw-ghapp
Copy link
Contributor

bw-ghapp bot commented Nov 20, 2025

⚠️ Changes in this PR impact the Autofill experience of the browser client ⚠️

BIT has tested the core experience with these changes and all feature flags disabled.

Caution

Unfortunately, one or more of these tests failed. 😞

Please resolve the failure before merging; reach out to @bitwarden/team-autofill-dev if you'd like help.

You can view the detailed results of the tests here.

Comment on lines +51 to +57
export const IGNORED_PHISHING_HOSTNAMES_KEY = new KeyDefinition<string[]>(
PHISHING_DETECTION_DISK,
"ignoredPhishingHostnames",
{
deserializer: (value: string[]) => value ?? [],
},
);
Copy link
Contributor

Choose a reason for hiding this comment

The 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 const IGNORED_PHISHING_HOSTNAMES_KEY = new KeyDefinition<string[]>(
PHISHING_DETECTION_DISK,
"ignoredPhishingHostnames",
{
deserializer: (value: string[]) => value ?? [],
},
);
export const IGNORED_PHISHING_HOSTNAMES_KEY = KeyDefinition.array<string>(
PHISHING_DETECTION_DISK,
"ignoredPhishingHostnames",
{
deserializer: (value: string) => value,
},
);

.pipe(
map((hostnames) => hostnames ?? []),
tap((hostnames) => {
this._ignoredHostnames = new Set(hostnames);
Copy link
Contributor

Choose a reason for hiding this comment

The 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}`),
      ),

Comment on lines +105 to +107
this._ignoredHostnames.add(url.hostname);
// Persist to storage
await ignoredHostnamesState.update(() => Array.from(this._ignoredHostnames));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: this is susceptible to race-conditions because awaiting ignoredHostnamesState.update can cause the execution of this function to be pre-empted by e.g. an emission of ignoredHostnamesState.state$ which would cause this._ignoredHostnames to get overwritten before the anonymous function inside update(...) has a chance to fetch the value

Either stash a copy of the Set locally or, better, don't keep a copy of state data outside of state providers and subscribe to it instead

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants