mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: verify credentials upstream before minting identity cookies (GHSA-wxcm-j4jc-9fxq)
This commit is contained in:
@@ -4,12 +4,7 @@ import { logger } from '@/lib/logger';
|
||||
import { encryptSession, decryptSession } from '@/lib/auth/crypto';
|
||||
import { SESSION_COOKIE_MAX_AGE, sessionCookieName } from '@/lib/auth/session-cookie';
|
||||
import { getCookieOptions } from '@/lib/oauth/cookie-config';
|
||||
import {
|
||||
JmapAuthVerificationError,
|
||||
normalizeJmapServerUrl,
|
||||
validateProxyAuthHeader,
|
||||
verifyJmapAuth,
|
||||
} from '@/lib/auth/verify-jmap-auth';
|
||||
import { JmapAuthVerificationError, verifyJmapIdentity } from '@/lib/auth/verify-jmap-auth';
|
||||
import {
|
||||
clearStalwartAuthContextInStore,
|
||||
setStalwartAuthContextInStore,
|
||||
@@ -86,13 +81,16 @@ export async function POST(request: NextRequest) {
|
||||
const slot = typeof bodySlot === 'number' && bodySlot >= 0 && bodySlot < MAX_ACCOUNT_SLOTS ? bodySlot : getSlot(request);
|
||||
const cookieName = sessionCookieName(slot);
|
||||
const authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||
// Trusted (admin-configured) URLs skip the upstream re-fetch: the cookie
|
||||
// we write here is only ever consumed for requests on behalf of this same
|
||||
// user, so bogus credentials would just yield 401s downstream rather than
|
||||
// privilege escalation. Untrusted custom endpoints still verify upstream.
|
||||
const normalizedServerUrl = upstreamTrusted
|
||||
? (validateProxyAuthHeader(authHeader), normalizeJmapServerUrl(upstreamUrl))
|
||||
: await verifyJmapAuth(upstreamUrl, authHeader, { trusted: false });
|
||||
// Always verify the credential upstream (GHSA-wxcm-j4jc-9fxq). The cookies
|
||||
// written here are not only replayed as credentials (where a bogus
|
||||
// password would just 401 downstream): the encrypted auth context is also
|
||||
// accepted as proof of identity by routes that never contact the mail
|
||||
// server, such as settings sync. Skipping the check for admin-configured
|
||||
// servers let anyone mint a cookie for any username with a made-up
|
||||
// password. `trusted` only relaxes the public-address requirement.
|
||||
const normalizedServerUrl = await verifyJmapIdentity(upstreamUrl, authHeader, username, {
|
||||
trusted: upstreamTrusted,
|
||||
});
|
||||
const token = encryptSession(normalizedServerUrl, username, password);
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(cookieName, token, sessionCookieOptions());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { JmapAuthVerificationError, assertBasicAuthMatchesUsername, normalizeJmapServerUrl, validateProxyAuthHeader, verifyJmapAuth } from '@/lib/auth/verify-jmap-auth';
|
||||
import { JmapAuthVerificationError, verifyJmapIdentity } from '@/lib/auth/verify-jmap-auth';
|
||||
import { setStalwartAuthContext } from '@/lib/stalwart/auth-context';
|
||||
import { configManager } from '@/lib/admin/config-manager';
|
||||
import { isPublicHttpUrl } from '@/lib/security/url-guard';
|
||||
@@ -62,22 +62,17 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const slot = getSlot(request, bodySlot);
|
||||
// Trusted (admin-configured) URLs skip the upstream re-fetch, but we
|
||||
// still bind the cookie's `username` to the credential when we can verify
|
||||
// locally. Without this, a caller can POST username="admin@host" +
|
||||
// authHeader=<their own Basic creds>, and downstream consumers that read
|
||||
// the cookie-derived username (audit logs, login tracker) accept the
|
||||
// spoof. Bearer tokens are opaque so only the format check runs;
|
||||
// authorization sinks must key off the credential itself, not the
|
||||
// cookie's username claim (see admin/auth's authHeader-hashed cache key).
|
||||
let normalizedServerUrl: string;
|
||||
if (upstreamTrusted) {
|
||||
validateProxyAuthHeader(authHeader);
|
||||
assertBasicAuthMatchesUsername(authHeader, username);
|
||||
normalizedServerUrl = normalizeJmapServerUrl(upstreamUrl);
|
||||
} else {
|
||||
normalizedServerUrl = await verifyJmapAuth(upstreamUrl, authHeader, { trusted: false });
|
||||
}
|
||||
// Always verify the credential upstream and bind the cookie's `username`
|
||||
// to it (GHSA-wxcm-j4jc-9fxq). The encrypted context this route writes is
|
||||
// accepted as proof of identity by routes that never contact the mail
|
||||
// server (settings sync, plugin-approval attribution), so minting it for
|
||||
// an unchecked username lets anyone without an account read and overwrite
|
||||
// any user's synced settings. Admin-configured servers are `trusted`,
|
||||
// which only relaxes the public-address requirement (they may live on a
|
||||
// private network) - never the credential check.
|
||||
const normalizedServerUrl = await verifyJmapIdentity(upstreamUrl, authHeader, username, {
|
||||
trusted: upstreamTrusted,
|
||||
});
|
||||
|
||||
await setStalwartAuthContext(slot, {
|
||||
serverUrl: normalizedServerUrl,
|
||||
|
||||
Reference in New Issue
Block a user