forked from QobuzDL/Qobuz-DL
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmiddleware.ts
More file actions
160 lines (139 loc) · 5.63 KB
/
Copy pathmiddleware.ts
File metadata and controls
160 lines (139 loc) · 5.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import { NextRequest, NextResponse } from 'next/server';
// --- CORS headers (needed for Monochrome and other cross-origin clients) ---
const CORS_HEADERS: Record<string, string> = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Token-Country, Music-Source',
};
function corsResponse(body: string | null, status: number, extra?: Record<string, string>): NextResponse {
return new NextResponse(body, { status, headers: { ...CORS_HEADERS, ...extra } });
}
// --- WHITELIST: only these paths are valid app routes ---
// Everything else gets 404 immediately (no SSR rendering = no returnNaN/let exploits)
const VALID_PATHS = [
/^\/$/, // home page
/^\/api\/(get-music|download-music|get-album|get-artist|get-releases|get-countries|get-apple-capabilities)(\/|$)/,
/^\/manifest/, // PWA manifest
/^\/flac\//, // public/flac
/^\/logo\//, // public/logo
/^\/_next\/webpack-hmr/, // HMR (dev only, harmless in prod)
];
// --- Malicious Payload Detection ---
const MALICIOUS_PATTERNS = [
/\beval\s*\(/i,
/\bFunction\s*\(/i,
/\brequire\s*\(/i,
/\bimport\s*\(/i,
/\b__proto__\b/i,
/\bconstructor\s*\[/i,
/\bprocess\.env/i,
/\bchild_process/i,
/\bexec\s*\(/i,
/\bspawn\s*\(/i,
/\.\.\/|\.\.\\/, // path traversal
/<script/i,
/javascript:/i,
/\bon\w+\s*=/i, // event handlers
/\breturnNaN\b/i,
];
// Bot user agents to block on API routes
const BLOCKED_UA_PATTERNS = [
/sqlmap/i,
/nikto/i,
/nmap/i,
/masscan/i,
/zgrab/i,
/gobuster/i,
/dirbuster/i,
/nuclei/i,
/httpx/i,
/scrapy/i,
/python-requests\/[0-9]/i,
/go-http-client/i,
/java\/[0-9]/i,
];
// Hardcoded blocked IPs (known attackers)
const BLOCKED_IPS = new Set([
'2600:387:15:3613::1', // Residential proxy bot - returnNaN/let attacks
'45.94.31.32', // WordPress scanner bot - 1337 Services GmbH NL (AbuseIPDB flagged)
'45.205.1.43', // SSR injection bot - POST / with returnNaN payload, rotating UAs
'212.113.98.30', // Cryptominer dropper - POST / with base64 bash payload downloading from 78.153.140.16
'34.246.163.208', // WordPress scanner bot - Tentou por muito tempo varios endpoints, como .env e outros
'35.240.247.11', // WordPress scanner bot - (AbuseIPDB flagged)
'164.92.178.95', // .env & docker scanner bot - DigitalOcean droplet
'193.32.162.28', // Tentou acessar uma vez /api/route - provavelmente um scanner de vulnerabilidades (AbuseIPDB flagged)
]);
function getClientIp(request: NextRequest): string {
return (
request.headers.get('cf-connecting-ip') ||
request.headers.get('x-real-ip') ||
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
'unknown'
);
}
function containsMaliciousPayload(url: string, params: URLSearchParams): boolean {
const fullString = url + ' ' + Array.from(params.values()).join(' ');
return MALICIOUS_PATTERNS.some((pattern) => pattern.test(fullString));
}
function isBlockedBot(ua: string | null): boolean {
if (!ua) return true; // No UA = bot
return BLOCKED_UA_PATTERNS.some((pattern) => pattern.test(ua));
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Skip only static assets — matcher already excludes _next/static and _next/image
// DO NOT skip all /_next/ — attackers exploit /_next/data/BUILD_ID/returnNaN.json
if (
pathname.startsWith('/favicon') ||
pathname.match(/\.(ico|png|jpg|jpeg|svg|webp|css|woff2?|ttf|map)$/)
) {
return NextResponse.next();
}
// Handle CORS preflight
if (request.method === 'OPTIONS') {
return corsResponse(null, 204);
}
// Block non-GET/HEAD methods on non-API routes (POST / is used for SSR injection)
if (!pathname.startsWith('/api/') && request.method !== 'GET' && request.method !== 'HEAD') {
return corsResponse('Method Not Allowed', 405);
}
const ip = getClientIp(request);
const ua = request.headers.get('user-agent');
// 1. Block hardcoded attacker IPs
if (BLOCKED_IPS.has(ip)) {
return corsResponse('Forbidden', 403);
}
// 2. WHITELIST — block any path that isn't a known valid route
// This kills returnNaN/let/directory-traversal attacks at the gate
if (!VALID_PATHS.some((pattern) => pattern.test(pathname))) {
console.warn(`[middleware] Blocked: ${ip} → ${pathname}`);
return corsResponse('Not Found', 404);
}
// 3. Block known malicious bots (API routes only — pages need browsers)
if (pathname.startsWith('/api/') && isBlockedBot(ua)) {
return corsResponse('Forbidden', 403);
}
// 4. Malicious payload detection (URL + query params)
const params = request.nextUrl.searchParams;
if (containsMaliciousPayload(request.url, params)) {
return corsResponse('Bad Request', 400);
}
// 5. Validate query params for search endpoints
if (pathname === '/api/get-music') {
const q = params.get('q');
if (q && q.length > 500) {
return corsResponse(JSON.stringify({ error: 'Query too long' }), 400, {
'Content-Type': 'application/json',
});
}
}
// Pass through — add CORS headers to the response
const response = NextResponse.next();
for (const [key, value] of Object.entries(CORS_HEADERS)) {
response.headers.set(key, value);
}
return response;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};