fix: add SSRF protection to URL preview endpoint
Block requests to private/reserved IP ranges (10.x, 172.16-31.x, 192.168.x, 127.x, 169.254.x, localhost, ::1) and non-http(s) schemes in the /api/apps/preview endpoint to prevent server-side request forgery.
This commit is contained in:
@@ -10,6 +10,37 @@ const previewSchema = z.object({
|
||||
|
||||
const PREVIEW_TIMEOUT_MS = 10_000;
|
||||
|
||||
/**
|
||||
* Block requests to private/reserved IP ranges to prevent SSRF.
|
||||
*/
|
||||
function isPrivateOrReservedHost(hostname: string): boolean {
|
||||
if (
|
||||
hostname === 'localhost' ||
|
||||
hostname === '127.0.0.1' ||
|
||||
hostname === '::1' ||
|
||||
hostname === '0.0.0.0'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const ipv4Match = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
||||
if (ipv4Match) {
|
||||
const [, a, b] = ipv4Match.map(Number);
|
||||
if (a === 10) return true;
|
||||
if (a === 172 && b >= 16 && b <= 31) return true;
|
||||
if (a === 192 && b === 168) return true;
|
||||
if (a === 127) return true;
|
||||
if (a === 169 && b === 254) return true;
|
||||
if (a === 0) return true;
|
||||
}
|
||||
|
||||
if (hostname.startsWith('fe80:') || hostname.startsWith('fc') || hostname.startsWith('fd')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the page title from HTML content.
|
||||
*/
|
||||
@@ -71,6 +102,21 @@ export const POST: RequestHandler = async (event) => {
|
||||
|
||||
const { url } = parsed.data;
|
||||
|
||||
// SSRF protection: block private/reserved IPs and non-http(s) schemes
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
||||
return json(error('Only http and https URLs are allowed'), { status: 400 });
|
||||
}
|
||||
if (isPrivateOrReservedHost(parsedUrl.hostname)) {
|
||||
return json(error('URLs pointing to private or reserved IP ranges are not allowed'), {
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
return json(error('Invalid URL'), { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
// HEAD request for status and timing
|
||||
const startTime = Date.now();
|
||||
|
||||
Reference in New Issue
Block a user