fix: validate URLs before outbound fetch

This commit is contained in:
Linus Rath
2026-04-27 22:23:39 +02:00
parent e9b3eacbb7
commit 3043639d2d
6 changed files with 379 additions and 90 deletions
+41 -6
View File
@@ -1,4 +1,7 @@
import { isPublicHttpUrl } from '@/lib/security/url-guard';
const VERIFY_TIMEOUT_MS = 10000;
const MAX_REDIRECTS = 3;
export class JmapAuthVerificationError extends Error {
status: number;
@@ -41,15 +44,47 @@ export async function verifyJmapAuth(serverUrl: string, authHeader: string): Pro
const normalizedServerUrl = normalizeJmapServerUrl(serverUrl);
validateProxyAuthHeader(authHeader);
if (!(await isPublicHttpUrl(normalizedServerUrl))) {
throw new JmapAuthVerificationError('Server URL is not allowed', 400);
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), VERIFY_TIMEOUT_MS);
try {
const response = await fetch(`${normalizedServerUrl}/.well-known/jmap`, {
method: 'GET',
headers: { Authorization: authHeader },
signal: controller.signal,
});
let currentUrl = `${normalizedServerUrl}/.well-known/jmap`;
let response: Response | undefined;
for (let i = 0; i <= MAX_REDIRECTS; i++) {
if (!(await isPublicHttpUrl(currentUrl))) {
throw new JmapAuthVerificationError('Server URL is not allowed', 400);
}
response = await fetch(currentUrl, {
method: 'GET',
headers: { Authorization: authHeader },
signal: controller.signal,
redirect: 'manual',
});
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get('location');
if (!location) {
throw new JmapAuthVerificationError('Failed to verify JMAP session', 502);
}
currentUrl = new URL(location, currentUrl).toString();
continue;
}
break;
}
if (!response) {
throw new JmapAuthVerificationError('Failed to verify JMAP session', 502);
}
if (response.status >= 300 && response.status < 400) {
throw new JmapAuthVerificationError('Too many redirects verifying JMAP session', 502);
}
if (!response.ok) {
throw new JmapAuthVerificationError(
@@ -77,4 +112,4 @@ export async function verifyJmapAuth(serverUrl: string, authHeader: string): Pro
} finally {
clearTimeout(timeout);
}
}
}