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:
2025-12-05 14:21:52 -05:00
parent 55e4c5ff0c
commit e18aa3f42c
44 changed files with 3512 additions and 892 deletions

View File

@@ -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);