From 23b23f39b97631f01fd67dec7edc5e1f261c7651 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Wed, 24 Jun 2026 01:23:34 +0800 Subject: [PATCH] fix: require reauthentication for auth request approval --- src/handlers/auth-requests.ts | 16 ++++++++++++++++ src/handlers/identity.ts | 5 ++++- webapp/src/App.tsx | 35 +++++++++++++++++++++++++++-------- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/handlers/auth-requests.ts b/src/handlers/auth-requests.ts index 04a63e4..e29a8ca 100644 --- a/src/handlers/auth-requests.ts +++ b/src/handlers/auth-requests.ts @@ -14,6 +14,19 @@ function normalizeText(value: unknown, maxLength: number): string { return String(value ?? '').trim().slice(0, maxLength); } +function isSerializedEncString(value: unknown): value is string { + const text = String(value || '').trim(); + if (!text) return false; + const parts = text.split('.'); + if (parts.length !== 2) return false; + const type = Number(parts[0]); + const bodyParts = parts[1].split('|'); + if (type === 2) return bodyParts.length === 3 && bodyParts.every(Boolean); + if (type === 3 || type === 4) return bodyParts.length === 1 && !!bodyParts[0]; + if (type === 5 || type === 6) return bodyParts.length === 2 && bodyParts.every(Boolean); + return false; +} + function getClientIp(request: Request): string | null { return ( request.headers.get('CF-Connecting-IP') || @@ -251,6 +264,9 @@ export async function handleUpdateAuthRequest(request: Request, env: Env, userId if (approved && !key) { return errorResponse('Encrypted key is required to approve the request.', 400); } + if (approved && !isSerializedEncString(key)) { + return errorResponse('Encrypted key is not a valid encrypted string.', 400); + } const updated = await storage.updateAuthRequestResponse(id, userId, { approved, diff --git a/src/handlers/identity.ts b/src/handlers/identity.ts index e584e28..0fd843d 100644 --- a/src/handlers/identity.ts +++ b/src/handlers/identity.ts @@ -337,6 +337,7 @@ export async function handleToken(request: Request, env: Env): Promise } let validatedAuthRequestId: string | null = null; + let authRequestLoginKey: string | null = null; let valid = false; const normalizedAuthRequestId = String(authRequestId || '').trim(); if (normalizedAuthRequestId) { @@ -349,10 +350,12 @@ export async function handleToken(request: Request, env: Env): Promise authRequest.responseDate && !authRequest.authenticationDate && !isAuthRequestExpired(authRequest) && + !!authRequest.key && constantTimeEquals(authRequest.accessCode, passwordHash) ); if (valid) { validatedAuthRequestId = authRequest!.id; + authRequestLoginKey = authRequest!.key; } } else { valid = await auth.verifyPassword(passwordHash, user.masterPasswordHash, user.email); @@ -493,7 +496,7 @@ export async function handleToken(request: Request, env: Env): Promise token_type: 'Bearer', ...(shouldUseWebSession(request) ? { web_session: true } : { refresh_token: refreshToken }), ...(trustedTwoFactorTokenToReturn ? { TwoFactorToken: trustedTwoFactorTokenToReturn } : {}), - Key: user.key, + Key: authRequestLoginKey || user.key, PrivateKey: user.privateKey, AccountKeys: accountKeys, accountKeys: accountKeys, diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index d27aa54..2392e62 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -238,6 +238,7 @@ export default function App() { const [disableTotpPassword, setDisableTotpPassword] = useState(''); const [disableTotpSubmitting, setDisableTotpSubmitting] = useState(false); const [authRequestDialogDismissedId, setAuthRequestDialogDismissedId] = useState(null); + const [authRequestDialogSelectedId, setAuthRequestDialogSelectedId] = useState(null); const [authRequestSubmittingId, setAuthRequestSubmittingId] = useState(null); const [recoverValues, setRecoverValues] = useState({ email: '', password: '', recoveryCode: '' }); const [themePreference, setThemePreference] = useState(() => readThemePreference()); @@ -1119,7 +1120,20 @@ export default function App() { }); const pendingAuthRequests = (pendingAuthRequestsQuery.data || []).filter(isPendingAuthRequest); const latestPendingAuthRequest = pendingAuthRequests[0] || null; - const authRequestDialogOpen = !!latestPendingAuthRequest && latestPendingAuthRequest.id !== authRequestDialogDismissedId; + const selectedPendingAuthRequest = authRequestDialogSelectedId + ? pendingAuthRequests.find((request) => request.id === authRequestDialogSelectedId) || null + : null; + const authRequestDialogRequest = selectedPendingAuthRequest || ( + latestPendingAuthRequest && latestPendingAuthRequest.id !== authRequestDialogDismissedId + ? latestPendingAuthRequest + : null + ); + const authRequestDialogOpen = !!authRequestDialogRequest; + + async function beginApproveAuthRequest(authRequest: AuthRequest): Promise { + setAuthRequestDialogSelectedId(authRequest.id); + setAuthRequestDialogDismissedId(null); + } async function approveAuthRequest(authRequest: AuthRequest): Promise { if (!session) throw new Error(t('txt_vault_key_unavailable')); @@ -1133,6 +1147,7 @@ export default function App() { requestApproved: true, }); setAuthRequestDialogDismissedId(null); + setAuthRequestDialogSelectedId(null); pushToast('success', t('txt_auth_request_approved')); await pendingAuthRequestsQuery.refetch(); } finally { @@ -1148,6 +1163,7 @@ export default function App() { requestApproved: false, }); setAuthRequestDialogDismissedId(null); + setAuthRequestDialogSelectedId(null); pushToast('success', t('txt_auth_request_denied')); await pendingAuthRequestsQuery.refetch(); } finally { @@ -2002,7 +2018,7 @@ export default function App() { onRefreshPendingAuthRequests: async () => { await pendingAuthRequestsQuery.refetch(); }, - onApproveAuthRequest: approveAuthRequest, + onApproveAuthRequest: beginApproveAuthRequest, onDenyAuthRequest: denyAuthRequest, onLockTimeoutChange: setLockTimeoutMinutes, onSessionTimeoutActionChange: setSessionTimeoutAction, @@ -2278,21 +2294,24 @@ export default function App() { /> { - if (!latestPendingAuthRequest) return; - void approveAuthRequest(latestPendingAuthRequest).catch((error) => { + if (!authRequestDialogRequest) return; + void approveAuthRequest(authRequestDialogRequest).catch((error) => { pushToast('error', error instanceof Error ? error.message : t('txt_auth_request_update_failed')); }); }} onDeny={() => { - if (!latestPendingAuthRequest) return; - void denyAuthRequest(latestPendingAuthRequest).catch((error) => { + if (!authRequestDialogRequest) return; + void denyAuthRequest(authRequestDialogRequest).catch((error) => { pushToast('error', error instanceof Error ? error.message : t('txt_auth_request_update_failed')); }); }} - onClose={() => setAuthRequestDialogDismissedId(latestPendingAuthRequest?.id || null)} + onClose={() => { + setAuthRequestDialogSelectedId(null); + setAuthRequestDialogDismissedId(authRequestDialogRequest?.id || null); + }} /> );