feat(api): implement rate limiting and SSRF protection across endpoints
- Added rate limiting to `reaction-users`, `search`, and `image-proxy` APIs to prevent abuse. - Introduced SSRF protection in `image-proxy` to block requests to private IP ranges. - Enhanced `link-preview` to use `linkedom` for HTML parsing and improved meta tag extraction. - Refactored authentication checks in various pages to utilize middleware for cleaner code. - Improved JWT key loading with error handling and security warnings for production. - Updated `authFetch` utility to handle token refresh more efficiently with deduplication. - Enhanced rate limiting utility to trust proxy headers from known sources. - Numerous layout / design changes
This commit is contained in:
@@ -12,8 +12,38 @@ import {
|
||||
createNonceCookie,
|
||||
} from '../../utils/rateLimit.js';
|
||||
|
||||
// Secret key for signing URLs - in production, use an environment variable
|
||||
const SIGNING_SECRET = process.env.IMAGE_PROXY_SECRET || 'dev-secret-change-me';
|
||||
// Secret key for signing URLs - MUST be set in production
|
||||
const SIGNING_SECRET = import.meta.env.IMAGE_PROXY_SECRET;
|
||||
if (!SIGNING_SECRET) {
|
||||
console.error('CRITICAL: IMAGE_PROXY_SECRET environment variable is not set!');
|
||||
}
|
||||
|
||||
// Private IP ranges to block (SSRF protection)
|
||||
const PRIVATE_IP_PATTERNS = [
|
||||
/^127\./, // localhost
|
||||
/^10\./, // Class A private
|
||||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Class B private
|
||||
/^192\.168\./, // Class C private
|
||||
/^169\.254\./, // Link-local
|
||||
/^0\./, // Current network
|
||||
/^224\./, // Multicast
|
||||
/^255\./, // Broadcast
|
||||
/^localhost$/i,
|
||||
/^\[?::1\]?$/, // IPv6 localhost
|
||||
/^\[?fe80:/i, // IPv6 link-local
|
||||
/^\[?fc00:/i, // IPv6 private
|
||||
/^\[?fd00:/i, // IPv6 private
|
||||
];
|
||||
|
||||
function isPrivateUrl(urlString) {
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
const hostname = url.hostname;
|
||||
return PRIVATE_IP_PATTERNS.some(pattern => pattern.test(hostname));
|
||||
} catch {
|
||||
return true; // Block invalid URLs
|
||||
}
|
||||
}
|
||||
|
||||
// Max image size to proxy (25MB - needed for animated GIFs)
|
||||
const MAX_IMAGE_SIZE = 25 * 1024 * 1024;
|
||||
@@ -136,6 +166,11 @@ export async function GET({ request }) {
|
||||
return new Response('Missing signature', { status: 403 });
|
||||
}
|
||||
|
||||
// Require signing secret to be configured
|
||||
if (!SIGNING_SECRET) {
|
||||
return new Response('Server misconfigured', { status: 500 });
|
||||
}
|
||||
|
||||
// Verify the signature
|
||||
const isValid = await verifySignature(imageUrl, signature);
|
||||
if (!isValid) {
|
||||
@@ -153,6 +188,11 @@ export async function GET({ request }) {
|
||||
return new Response('Invalid URL', { status: 400 });
|
||||
}
|
||||
|
||||
// SSRF protection: block private/internal IPs
|
||||
if (isPrivateUrl(imageUrl)) {
|
||||
return new Response('URL not allowed', { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
Reference in New Issue
Block a user