fix: patch critical auth bypass and credential leak vulnerabilities
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getPlugin } from '@/lib/admin/plugin-registry';
|
import { getPlugin } from '@/lib/admin/plugin-registry';
|
||||||
import { getPluginConfig, setPluginConfig, deletePluginConfigKey } from '@/lib/admin/plugin-config';
|
import { getPluginConfig, setPluginConfig, deletePluginConfigKey } from '@/lib/admin/plugin-config';
|
||||||
|
import { requireAdminAuth } from '@/lib/admin/session';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/admin/plugins/[id]/config — Read all config for a plugin
|
* GET /api/admin/plugins/[id]/config — Read all config for a plugin
|
||||||
@@ -44,6 +45,9 @@ export async function PUT(
|
|||||||
{ params }: { params: Promise<{ id: string }> },
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const result = await requireAdminAuth();
|
||||||
|
if ('error' in result) return result.error;
|
||||||
|
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(id)) {
|
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(id)) {
|
||||||
@@ -88,6 +92,9 @@ export async function DELETE(
|
|||||||
{ params }: { params: Promise<{ id: string }> },
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const result = await requireAdminAuth();
|
||||||
|
if ('error' in result) return result.error;
|
||||||
|
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(id)) {
|
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(id)) {
|
||||||
|
|||||||
@@ -59,6 +59,45 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Invalid session' }, { status: 401 });
|
return NextResponse.json({ error: 'Invalid session' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only return non-sensitive fields. Use PUT to retrieve full credentials.
|
||||||
|
const { serverUrl, username } = credentials;
|
||||||
|
return NextResponse.json(
|
||||||
|
{ serverUrl, username },
|
||||||
|
{ headers: { 'Cache-Control': 'no-store, no-cache, must-revalidate' } },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Session read error', { error: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT — retrieve full credentials (including password) for session restoration.
|
||||||
|
* Protected by Sec-Fetch-Site to ensure only same-origin browser requests succeed.
|
||||||
|
*/
|
||||||
|
export async function PUT(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
// Block non-browser and cross-origin requests
|
||||||
|
const secFetchSite = request.headers.get('sec-fetch-site');
|
||||||
|
if (secFetchSite !== 'same-origin') {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const slot = getSlot(request);
|
||||||
|
const cookieName = sessionCookieName(slot);
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get(cookieName)?.value;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json({ error: 'No session' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = decryptSession(token);
|
||||||
|
if (!credentials) {
|
||||||
|
cookieStore.delete(cookieName);
|
||||||
|
return NextResponse.json({ error: 'Invalid session' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(credentials, {
|
return NextResponse.json(credentials, {
|
||||||
headers: { 'Cache-Control': 'no-store, no-cache, must-revalidate' },
|
headers: { 'Cache-Control': 'no-store, no-cache, must-revalidate' },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,16 +53,14 @@ function isEnabled(): boolean {
|
|||||||
/**
|
/**
|
||||||
* Verify identity against session cookies across all account slots.
|
* Verify identity against session cookies across all account slots.
|
||||||
* With multi-account, the requesting account may be on any slot (0-4).
|
* With multi-account, the requesting account may be on any slot (0-4).
|
||||||
* Returns true if any slot matches OR if no session cookies exist at all.
|
* Returns true only if a matching session cookie is found.
|
||||||
*/
|
*/
|
||||||
async function verifyIdentity(username: string, serverUrl: string): Promise<boolean> {
|
async function verifyIdentity(username: string, serverUrl: string): Promise<boolean> {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
let hasAnyCookie = false;
|
|
||||||
|
|
||||||
for (let slot = 0; slot <= 4; slot++) {
|
for (let slot = 0; slot <= 4; slot++) {
|
||||||
const token = cookieStore.get(sessionCookieName(slot))?.value;
|
const token = cookieStore.get(sessionCookieName(slot))?.value;
|
||||||
if (!token) continue;
|
if (!token) continue;
|
||||||
hasAnyCookie = true;
|
|
||||||
|
|
||||||
const session = decryptSession(token);
|
const session = decryptSession(token);
|
||||||
if (session && session.username === username && session.serverUrl === serverUrl) {
|
if (session && session.username === username && session.serverUrl === serverUrl) {
|
||||||
@@ -70,10 +68,7 @@ async function verifyIdentity(username: string, serverUrl: string): Promise<bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No cookies at all → can't verify, allow (same-origin protection applies)
|
// No matching session found (or no cookies at all) → reject
|
||||||
if (!hasAnyCookie) return true;
|
|
||||||
|
|
||||||
// Cookies exist but none matched → identity mismatch
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1014,7 +1014,7 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
scheduleRefresh(expires_in, get().refreshAccessToken, accountId);
|
scheduleRefresh(expires_in, get().refreshAccessToken, accountId);
|
||||||
}
|
}
|
||||||
} else if (targetAccount.authMode === 'basic' && targetAccount.rememberMe) {
|
} else if (targetAccount.authMode === 'basic' && targetAccount.rememberMe) {
|
||||||
const res = await fetch(`/api/auth/session?slot=${targetAccount.cookieSlot}`);
|
const res = await fetch(`/api/auth/session?slot=${targetAccount.cookieSlot}`, { method: 'PUT' });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const { serverUrl, username, password } = await res.json();
|
const { serverUrl, username, password } = await res.json();
|
||||||
targetClient = new JMAPClient(serverUrl, username, password);
|
targetClient = new JMAPClient(serverUrl, username, password);
|
||||||
@@ -1179,7 +1179,7 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
throw new Error(`Token refresh failed: ${res.status}`);
|
throw new Error(`Token refresh failed: ${res.status}`);
|
||||||
}
|
}
|
||||||
} else if (account.authMode === 'basic' && account.rememberMe) {
|
} else if (account.authMode === 'basic' && account.rememberMe) {
|
||||||
const res = await fetch(`/api/auth/session?slot=${account.cookieSlot}`);
|
const res = await fetch(`/api/auth/session?slot=${account.cookieSlot}`, { method: 'PUT' });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const { serverUrl, username, password } = await res.json();
|
const { serverUrl, username, password } = await res.json();
|
||||||
const client = new JMAPClient(serverUrl, username, password);
|
const client = new JMAPClient(serverUrl, username, password);
|
||||||
@@ -1370,7 +1370,7 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
if (state.authMode === 'basic') {
|
if (state.authMode === 'basic') {
|
||||||
set({ isLoading: true, isRateLimited: false, rateLimitUntil: null });
|
set({ isLoading: true, isRateLimited: false, rateLimitUntil: null });
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/session');
|
const res = await fetch('/api/auth/session', { method: 'PUT' });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!data.serverUrl || !data.username || !data.password) {
|
if (!data.serverUrl || !data.username || !data.password) {
|
||||||
|
|||||||
Reference in New Issue
Block a user