From c6438747e30d9a3e28509bc915ea4145a2d19572 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Mon, 6 Jul 2026 18:45:54 +0800 Subject: [PATCH] Harden 2FA disable and website icon privacy --- package.json | 3 +++ src/handlers/accounts.ts | 10 +--------- src/router-public.ts | 14 ++++++++++++-- src/types/index.ts | 1 + webapp/src/components/vault/WebsiteIcon.tsx | 13 ++++++++----- webapp/src/lib/app-auth.ts | 17 +++++++++++++++-- webapp/src/lib/demo.empty.ts | 1 + webapp/src/lib/demo.ts | 1 + webapp/src/lib/types.ts | 1 + webapp/src/lib/website-icon-settings.ts | 9 +++++++++ 10 files changed, 52 insertions(+), 18 deletions(-) create mode 100644 webapp/src/lib/website-icon-settings.ts diff --git a/package.json b/package.json index e87be07..f331e20 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,9 @@ }, "ATTACHMENTS_KV": { "description": "Optional KV namespace fallback for attachment/send-file storage" + }, + "WEBSITE_ICONS_ENABLED": { + "description": "Optional: set to true to proxy website icons via third-party icon services. Defaults to disabled for vault-domain privacy." } } }, diff --git a/src/handlers/accounts.ts b/src/handlers/accounts.ts index 7d98b94..c40e27f 100644 --- a/src/handlers/accounts.ts +++ b/src/handlers/accounts.ts @@ -1161,16 +1161,8 @@ export async function handleDisableTwoFactorProvider(request: Request, env: Env, return errorResponse('Two-factor provider is not supported by this server.', 400); } - const key = normalizeTotpSecret(readBodyString(body, ['key', 'Key'])); - const userVerificationToken = readBodyString(body, ['userVerificationToken', 'UserVerificationToken']); const secret = readBodyString(body, ['masterPasswordHash', 'MasterPasswordHash', 'otp', 'OTP', 'secret', 'Secret']); - let verified = false; - if (key && userVerificationToken) { - verified = await verifyTotpUserVerificationToken(env, user, key, userVerificationToken); - } - if (!verified) { - verified = await verifyUserSecret(auth, user, secret); - } + const verified = await verifyUserSecret(auth, user, secret); if (!verified) return errorResponse('User verification failed.', 400); if (type === TWO_FACTOR_PROVIDER_AUTHENTICATOR) { diff --git a/src/router-public.ts b/src/router-public.ts index 8b3cd9d..3688282 100644 --- a/src/router-public.ts +++ b/src/router-public.ts @@ -46,6 +46,11 @@ export interface WebBootstrapResponse { jwtSecretMinLength: number; registrationInviteRequired: boolean; webAuthnAllowedOrigins: string[]; + websiteIconsEnabled: boolean; +} + +function isWebsiteIconProxyEnabled(env: Env): boolean { + return ['1', 'true', 'yes', 'on'].includes(String(env.WEBSITE_ICONS_ENABLED || '').trim().toLowerCase()); } function isSameOriginWriteRequest(request: Request): boolean { @@ -257,7 +262,11 @@ function iconResponse(body: BodyInit | null, contentType: string | null): Respon }); } -async function handleWebsiteIcon(host: string, fallbackMode: 'default' | 'not-found' = 'default'): Promise { +async function handleWebsiteIcon(env: Env, host: string, fallbackMode: 'default' | 'not-found' = 'default'): Promise { + if (!isWebsiteIconProxyEnabled(env)) { + return fallbackMode === 'not-found' ? handleMissingWebsiteIcon() : handleNwFavicon(); + } + const normalizedHost = normalizeIconHost(host); if (!normalizedHost) return fallbackMode === 'not-found' ? handleMissingWebsiteIcon() : handleNwFavicon(); @@ -325,6 +334,7 @@ export async function buildWebBootstrapResponse(env: Env): Promise 0, webAuthnAllowedOrigins: getConfiguredWebAuthnAllowedOrigins(env), + websiteIconsEnabled: isWebsiteIconProxyEnabled(env), }; } @@ -375,7 +385,7 @@ export async function handlePublicRoute( const blocked = await enforcePublicRateLimit('public-icon', LIMITS.rateLimit.publicIconRequestsPerMinute); if (blocked) return blocked; const fallbackMode = new URL(request.url).searchParams.get('fallback') === '404' ? 'not-found' : 'default'; - return handleWebsiteIcon(iconMatch[1], fallbackMode); + return handleWebsiteIcon(env, iconMatch[1], fallbackMode); } const publicAttachmentMatch = path.match(/^\/api\/attachments\/([a-f0-9-]+)\/([a-f0-9-]+)$/i); diff --git a/src/types/index.ts b/src/types/index.ts index 7865144..95a95f4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -20,6 +20,7 @@ export interface Env { 'globalSettings__yubico__clientId'?: string; 'globalSettings__yubico__key'?: string; 'globalSettings__yubico__validationUrls'?: string; + WEBSITE_ICONS_ENABLED?: string; } export type UserRole = 'admin' | 'user'; diff --git a/webapp/src/components/vault/WebsiteIcon.tsx b/webapp/src/components/vault/WebsiteIcon.tsx index f14d811..5ad506c 100644 --- a/webapp/src/components/vault/WebsiteIcon.tsx +++ b/webapp/src/components/vault/WebsiteIcon.tsx @@ -10,6 +10,7 @@ import { } from '@/lib/website-icon-cache'; import { demoBrandIconUrl } from '@/lib/demo-brand-icons'; import { getCurrentNetworkStatus, subscribeNetworkStatus } from '@/lib/network-status'; +import { areWebsiteIconsEnabled } from '@/lib/website-icon-settings'; import { firstCipherUri, hostFromUri, websiteIconUrl } from '@/lib/website-utils'; const ICON_LOAD_ROOT_MARGIN = '180px 0px'; @@ -22,7 +23,8 @@ interface WebsiteIconProps { export default function WebsiteIcon(props: WebsiteIconProps) { const host = useMemo(() => hostFromUri(firstCipherUri(props.cipher)), [props.cipher]); - const src = host ? websiteIconUrl(host) : ''; + const iconsEnabled = areWebsiteIconsEnabled(); + const src = iconsEnabled && host ? websiteIconUrl(host) : ''; const nodeRef = useRef(null); const [shouldLoad, setShouldLoad] = useState(() => (host ? getWebsiteIconStatus(host) === 'loaded' : true)); const [status, setStatus] = useState(() => (host ? getWebsiteIconStatus(host) : 'idle')); @@ -33,7 +35,7 @@ export default function WebsiteIcon(props: WebsiteIconProps) { useEffect(() => subscribeNetworkStatus(setNetworkStatus), []); useEffect(() => { - if (!host) { + if (!host || !iconsEnabled) { setShouldLoad(true); setStatus('idle'); setImageUrl(''); @@ -47,7 +49,7 @@ export default function WebsiteIcon(props: WebsiteIconProps) { setStatus(next); setImageUrl(getWebsiteIconImageUrl(host)); }); - }, [host]); + }, [host, iconsEnabled]); useEffect(() => { if (!host || shouldLoad || status === 'loaded' || status === 'error') return; @@ -81,10 +83,11 @@ export default function WebsiteIcon(props: WebsiteIconProps) { useEffect(() => { if (SHOULD_LOAD_DEMO_BRAND_ICONS) return; if (demoIconUrl) return; + if (!iconsEnabled) return; if (networkStatus !== 'online') return; if (!host || !src || !shouldLoad || status !== 'idle') return; beginWebsiteIconLoad(host, src); - }, [demoIconUrl, host, networkStatus, src, shouldLoad, status]); + }, [demoIconUrl, host, iconsEnabled, networkStatus, src, shouldLoad, status]); if (demoIconUrl) { return ( @@ -100,7 +103,7 @@ export default function WebsiteIcon(props: WebsiteIconProps) { ); } - if (!host || status === 'error') { + if (!host || !iconsEnabled || status === 'error') { return {props.fallback ?? }; } diff --git a/webapp/src/lib/app-auth.ts b/webapp/src/lib/app-auth.ts index 030113c..1c325a5 100644 --- a/webapp/src/lib/app-auth.ts +++ b/webapp/src/lib/app-auth.ts @@ -27,6 +27,7 @@ import { unlockOfflineVaultWithMasterKey, } from '@/lib/offline-auth'; import { probeNodeWardenService } from '@/lib/network-status'; +import { setWebsiteIconsEnabled } from '@/lib/website-icon-settings'; import type { AccountPasskeyPrfOption, AppPhase, Profile, SessionState, TokenSuccess, WebBootstrapResponse } from '@/lib/types'; export interface PendingTotp { @@ -51,6 +52,7 @@ export type JwtUnsafeReason = 'missing' | 'too_short'; export interface BootstrapAppResult { defaultKdfIterations: number; registrationInviteRequired?: boolean; + websiteIconsEnabled: boolean; jwtWarning: { reason: JwtUnsafeReason; minLength: number } | null; session: SessionState | null; profile: Profile | null; @@ -61,6 +63,7 @@ export interface BootstrapAppResult { export interface InitialAppBootstrapState { defaultKdfIterations: number; registrationInviteRequired?: boolean; + websiteIconsEnabled: boolean; jwtWarning: { reason: JwtUnsafeReason; minLength: number } | null; session: SessionState | null; phase: AppPhase; @@ -229,10 +232,11 @@ function readWindowBootstrap(): WebBootstrapResponse { return raw && typeof raw === 'object' ? raw : {}; } -function normalizeBootstrapResponse(boot: WebBootstrapResponse): Pick { +function normalizeBootstrapResponse(boot: WebBootstrapResponse): Pick { const defaultKdfIterations = Number(boot.defaultKdfIterations || 600000); const registrationInviteRequired = typeof boot.registrationInviteRequired === 'boolean' ? boot.registrationInviteRequired : undefined; + const websiteIconsEnabled = boot.websiteIconsEnabled === true; const jwtUnsafeReason = boot.jwtUnsafeReason || null; const jwtWarning = jwtUnsafeReason ? { @@ -244,6 +248,7 @@ function normalizeBootstrapResponse(boot: WebBootstrapResponse): Pick