fix: harden proxy auth and SSRF defenses

This commit is contained in:
Linus Rath
2026-03-31 17:47:09 +02:00
parent b3d4c9241c
commit aa40c8be26
17 changed files with 462 additions and 175 deletions
+2 -8
View File
@@ -1,6 +1,6 @@
import { create } from 'zustand';
import { debug } from '@/lib/debug';
import { useAuthStore } from './auth-store';
import { getActiveAccountSlotHeaders } from '@/lib/auth/active-account-slot';
interface AccountSecurityState {
// Detection
@@ -44,13 +44,7 @@ interface AccountSecurityState {
}
function getApiHeaders(): Record<string, string> {
const { client } = useAuthStore.getState();
if (!client) return {};
return {
'Authorization': client.getAuthHeader(),
'X-JMAP-Server-URL': client.getServerUrl(),
'X-JMAP-Username': client.getUsername(),
};
return getActiveAccountSlotHeaders();
}
export const useAccountSecurityStore = create<AccountSecurityState>()((set, get) => ({
+49
View File
@@ -87,6 +87,27 @@ function getClientRateLimitState(client: IJMAPClient | null): Pick<AuthState, 'i
};
}
async function syncStalwartAuthContext(
serverUrl: string,
username: string,
authHeader: string,
slot: number,
): Promise<void> {
try {
const response = await fetch('/api/auth/stalwart-context', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ serverUrl, username, authHeader, slot }),
});
if (!response.ok) {
debug.warn('auth', `Failed to sync Stalwart auth context: ${response.status}`);
}
} catch (error) {
debug.warn('auth', 'Failed to sync Stalwart auth context:', error);
}
}
function bindClientStatusHandlers(
client: IJMAPClient,
set: (state: Partial<AuthState>) => void,
@@ -461,6 +482,8 @@ export const useAuthStore = create<AuthState>()(
}
}
await syncStalwartAuthContext(serverUrl, username, client.getAuthHeader(), cookieSlot);
set({
isAuthenticated: true,
isLoading: false,
@@ -636,6 +659,8 @@ export const useAuthStore = create<AuthState>()(
});
accountStore.setActiveAccount(accountId);
await syncStalwartAuthContext(serverUrl, username, client.getAuthHeader(), slot);
set({
isAuthenticated: true,
isLoading: false,
@@ -754,6 +779,9 @@ export const useAuthStore = create<AuthState>()(
});
accountStore.setActiveAccount(accountId);
const cookieSlot = accountStore.getAccountById(accountId)?.cookieSlot ?? 0;
await syncStalwartAuthContext(ssoServerUrl, username, client.getAuthHeader(), cookieSlot);
set({
isAuthenticated: true,
isLoading: false,
@@ -825,6 +853,15 @@ export const useAuthStore = create<AuthState>()(
get().client?.updateAccessToken(access_token);
if (account) {
await syncStalwartAuthContext(
account.serverUrl,
account.username,
`Bearer ${access_token}`,
slot,
);
}
set({
accessToken: access_token,
tokenExpiresAt: Date.now() + expires_in * 1000,
@@ -1012,6 +1049,12 @@ export const useAuthStore = create<AuthState>()(
await targetClient.connect();
clients.set(accountId, targetClient);
scheduleRefresh(expires_in, get().refreshAccessToken, accountId);
await syncStalwartAuthContext(
targetAccount.serverUrl,
targetAccount.username,
targetClient.getAuthHeader(),
targetAccount.cookieSlot,
);
}
} else if (targetAccount.authMode === 'basic' && targetAccount.rememberMe) {
const res = await fetch(`/api/auth/session?slot=${targetAccount.cookieSlot}`, { method: 'PUT' });
@@ -1021,6 +1064,7 @@ export const useAuthStore = create<AuthState>()(
bindClientStatusHandlers(targetClient, set, get, accountId);
await targetClient.connect();
clients.set(accountId, targetClient);
await syncStalwartAuthContext(serverUrl, username, targetClient.getAuthHeader(), targetAccount.cookieSlot);
}
}
} catch (err) {
@@ -1174,6 +1218,7 @@ export const useAuthStore = create<AuthState>()(
await client.connect();
clients.set(account.id, client);
scheduleRefresh(expires_in, get().refreshAccessToken, account.id);
await syncStalwartAuthContext(account.serverUrl, account.username, client.getAuthHeader(), account.cookieSlot);
accountStore.updateAccount(account.id, { isConnected: true, hasError: false });
} else {
throw new Error(`Token refresh failed: ${res.status}`);
@@ -1186,6 +1231,7 @@ export const useAuthStore = create<AuthState>()(
bindClientStatusHandlers(client, set, get, account.id);
await client.connect();
clients.set(account.id, client);
await syncStalwartAuthContext(serverUrl, username, client.getAuthHeader(), account.cookieSlot);
accountStore.updateAccount(account.id, { isConnected: true, hasError: false });
} else {
throw new Error(`Session cookie missing: ${res.status}`);
@@ -1401,6 +1447,9 @@ export const useAuthStore = create<AuthState>()(
});
accountStore.setActiveAccount(accountId);
const cookieSlot = accountStore.getAccountById(accountId)?.cookieSlot ?? 0;
await syncStalwartAuthContext(serverUrl, username, client.getAuthHeader(), cookieSlot);
const { identities, primaryIdentity } = loadIdentities(await client.getIdentities(), username);
initializeFeatureStores(client);