@@ -4,74 +4,94 @@ import {
44 normalizeIp ,
55 isBotUserAgent ,
66 isKnownBotIp ,
7+ isIpSuspicious ,
78 checkHeaders ,
89 checkHoneypot ,
910 validateBrowserSignature ,
1011 detectVpnProxy ,
1112 checkSuspiciousQueryParams ,
1213 checkBlockedCountry ,
1314 checkConcurrentRequests ,
14- validateRequestTiming ,
15+ checkReferrer ,
16+ checkBodySuspicious ,
17+ checkIpBlacklists ,
18+ calculateOverallRisk ,
19+ recordFailedAction ,
20+ isDisposableEmail ,
21+ checkPasswordBreach ,
22+ verifySubmitToken ,
1523} from '../services/security.js' ;
1624
17- const BLOCKED_COUNTRIES = new Set ( [ 'CN' , 'RU' , 'KP' , 'IR' , 'SY' , 'CU' , 'VE' ] ) ;
18-
19- const TIMING_EXEMPT_PATHS = [
20- '/api/config' , '/api/health' , '/api/auth/passkeys/login/begin' ,
21- '/api/auth/passkeys/login/complete' , '/api/auth/passkeys/register/begin' ,
22- '/api/auth/passkeys/register/complete' , '/api/auth/totp/verify' ,
23- '/api/auth/totp/recovery' , '/api/auth/check-availability' ,
24- '/api/auth/check-vpn' ,
25- ] ;
26-
2725const VPN_EXEMPT_PATHS = [
2826 '/api/config' , '/api/health' , '/api/activity' ,
2927] ;
3028
31- const SUSPICIOUS_IPS = new Set ( ) ;
29+ const SENSITIVE_PATHS = [
30+ '/api/auth/register' , '/api/auth/login' , '/api/auth/change-password' ,
31+ '/api/auth/change-email' , '/api/auth/delete-account' ,
32+ '/api/servers/create' , '/api/admin/login' ,
33+ ] ;
3234
33- export function advancedBotProtection ( required = false ) {
35+ export function advancedBotProtection ( ) {
3436 return async ( req , res , next ) => {
3537 try {
3638 if ( ! req . path . startsWith ( '/api/' ) ) return next ( ) ;
3739 const ip = getClientIp ( req ) ;
3840 const ua = ( req . headers [ 'user-agent' ] || '' ) . toString ( ) . slice ( 0 , 512 ) ;
39- const method = req . method ;
40- const isPost = [ 'POST' , 'PUT' , 'PATCH' , 'DELETE' ] . includes ( method ) ;
41+ const isPost = [ 'POST' , 'PUT' , 'PATCH' , 'DELETE' ] . includes ( req . method ) ;
4142
42- if ( SUSPICIOUS_IPS . has ( ip ) ) {
43- const attempts = SUSPICIOUS_IPS . get ( ip ) ;
44- if ( attempts >= 3 ) {
45- return res . status ( 403 ) . json ( { error : 'Access denied' , requestId : req . requestId } ) ;
46- }
43+ if ( isIpSuspicious ( ip ) && isPost ) {
44+ return res . status ( 403 ) . json ( { error : 'Access denied due to suspicious activity.' , requestId : req . requestId } ) ;
4745 }
4846
49- const { allowed : concurrentOk , count : concurrentCount } = checkConcurrentRequests ( ip , 8 ) ;
47+ const { allowed : concurrentOk } = checkConcurrentRequests ( ip , 6 ) ;
5048 if ( ! concurrentOk && isPost ) {
51- return res . status ( 429 ) . json ( { error : 'Too many simultaneous requests. Slow down. ' , requestId : req . requestId } ) ;
49+ return res . status ( 429 ) . json ( { error : 'Too many simultaneous requests.' , requestId : req . requestId } ) ;
5250 }
5351
5452 if ( isBotUserAgent ( ua ) && isPost ) {
55- SUSPICIOUS_IPS . set ( ip , ( SUSPICIOUS_IPS . get ( ip ) || 0 ) + 1 ) ;
56- return res . status ( 403 ) . json ( { error : 'Automated requests are not allowed. Please use a real browser. ' , requestId : req . requestId } ) ;
53+ recordFailedAction ( ip ) ;
54+ return res . status ( 403 ) . json ( { error : 'Automated requests are not allowed.' , requestId : req . requestId } ) ;
5755 }
5856
5957 if ( isKnownBotIp ( ip ) && isPost ) {
6058 return res . status ( 403 ) . json ( { error : 'Access denied' , requestId : req . requestId } ) ;
6159 }
6260
63- const { flagged : queryFlagged , param , pattern } = checkSuspiciousQueryParams ( req ) ;
61+ const { flagged : queryFlagged } = checkSuspiciousQueryParams ( req ) ;
6462 if ( queryFlagged && isPost ) {
65- console . warn ( `[SECURITY] Suspicious query param " ${ param } " containing " ${ pattern } " from IP ${ ip } ` ) ;
63+ recordFailedAction ( ip ) ;
6664 return res . status ( 400 ) . json ( { error : 'Invalid request parameters' , requestId : req . requestId } ) ;
6765 }
6866
69- const { triggered : honeypotTriggered , field : honeypotField } = checkHoneypot ( req . body ) ;
67+ const { triggered : honeypotTriggered } = checkHoneypot ( req . body ) ;
7068 if ( honeypotTriggered && isPost ) {
71- console . warn ( `[SECURITY] Honeypot triggered on field " ${ honeypotField } " from IP ${ ip } ` ) ;
69+ recordFailedAction ( ip ) ;
7270 return res . status ( 400 ) . json ( { error : 'Invalid form submission' , requestId : req . requestId } ) ;
7371 }
7472
73+ const bodyCheck = checkBodySuspicious ( req . body ) ;
74+ if ( bodyCheck . flagged && isPost ) {
75+ recordFailedAction ( ip ) ;
76+ return res . status ( 400 ) . json ( { error : 'Request contains invalid content' , requestId : req . requestId } ) ;
77+ }
78+
79+ if ( isPost && SENSITIVE_PATHS . includes ( req . path ) ) {
80+ const referrerCheck = checkReferrer ( req ) ;
81+ if ( ! referrerCheck . passed ) {
82+ recordFailedAction ( ip ) ;
83+ return res . status ( 403 ) . json ( { error : 'Invalid request origin' , requestId : req . requestId } ) ;
84+ }
85+ }
86+
87+ if ( isPost ) {
88+ const risk = calculateOverallRisk ( req ) ;
89+ if ( risk . level === 'high' ) {
90+ recordFailedAction ( ip ) ;
91+ return res . status ( 403 ) . json ( { error : 'Request blocked for security reasons' , requestId : req . requestId } ) ;
92+ }
93+ }
94+
7595 next ( ) ;
7696 } catch ( err ) {
7797 console . error ( '[SECURITY] Middleware error:' , err . message ) ;
@@ -90,17 +110,26 @@ export function vpnProxyProtection() {
90110 if ( ! cleanIp || isPrivateIp ( cleanIp ) ) return next ( ) ;
91111 const isPost = [ 'POST' , 'PUT' , 'PATCH' , 'DELETE' ] . includes ( req . method ) ;
92112 if ( ! isPost ) return next ( ) ;
93- const result = await detectVpnProxy ( cleanIp ) ;
94- if ( result . isVpn || result . isProxy || result . isTor ) {
95- const source = result . source || 'unknown' ;
96- console . warn ( `[SECURITY] VPN/Proxy/Tor detected for IP ${ cleanIp } (source: ${ source } )` ) ;
113+ const [ vpnResult , blacklistResult ] = await Promise . all ( [
114+ detectVpnProxy ( cleanIp ) ,
115+ req . path . startsWith ( '/api/auth/' ) || req . path . startsWith ( '/api/servers/' ) ? checkIpBlacklists ( cleanIp ) : { listed : false } ,
116+ ] ) ;
117+ if ( vpnResult . isVpn || vpnResult . isProxy || vpnResult . isTor ) {
118+ recordFailedAction ( ip ) ;
97119 return res . status ( 403 ) . json ( {
98- error : result . isTor
120+ error : vpnResult . isTor
99121 ? 'Tor network access is not allowed.'
100122 : 'VPN or proxy detected. Please disable your VPN for security reasons.' ,
101123 requestId : req . requestId ,
102124 } ) ;
103125 }
126+ if ( blacklistResult . listed ) {
127+ recordFailedAction ( ip ) ;
128+ return res . status ( 403 ) . json ( {
129+ error : 'Access denied. Your IP has been flagged.' ,
130+ requestId : req . requestId ,
131+ } ) ;
132+ }
104133 next ( ) ;
105134 } catch ( err ) {
106135 console . error ( '[SECURITY] VPN proxy middleware error:' , err . message ) ;
@@ -119,7 +148,7 @@ export function countryBlock() {
119148 const ip = getClientIp ( req ) ;
120149 const { blocked, countryCode } = await checkBlockedCountry ( ip ) ;
121150 if ( blocked ) {
122- console . warn ( `[SECURITY] Blocked request from ${ countryCode } IP ${ ip } ` ) ;
151+ recordFailedAction ( ip ) ;
123152 return res . status ( 403 ) . json ( { error : 'Service not available in your region.' , requestId : req . requestId } ) ;
124153 }
125154 next ( ) ;
@@ -138,14 +167,13 @@ export function browserIntegrityCheck() {
138167 if ( ! isPost ) return next ( ) ;
139168 const issues = checkHeaders ( req ) ;
140169 if ( issues . length >= 3 ) {
141- const ip = getClientIp ( req ) ;
142- console . warn ( `[SECURITY] Browser integrity check failed for IP ${ ip } : ${ issues . join ( ', ' ) } ` ) ;
170+ recordFailedAction ( getClientIp ( req ) ) ;
143171 return res . status ( 403 ) . json ( { error : 'Invalid request headers. Please use a real browser.' , requestId : req . requestId } ) ;
144172 }
145173 const signature = validateBrowserSignature ( req ) ;
146- if ( ! signature . passed && issues . length > 0 ) {
147- const ip = getClientIp ( req ) ;
148- console . warn ( `[SECURITY] Low browser signature score ${ signature . total } /100 for IP ${ ip } ` ) ;
174+ if ( signature . total < 30 && issues . length > 0 ) {
175+ recordFailedAction ( getClientIp ( req ) ) ;
176+ return res . status ( 403 ) . json ( { error : 'Browser verification failed. Please use a modern browser.' , requestId : req . requestId } ) ;
149177 }
150178 next ( ) ;
151179 } catch ( err ) {
@@ -155,13 +183,52 @@ export function browserIntegrityCheck() {
155183 } ;
156184}
157185
186+ export function disposableEmailCheck ( ) {
187+ return async ( req , res , next ) => {
188+ try {
189+ if ( ! req . path . startsWith ( '/api/' ) ) return next ( ) ;
190+ const email = req . body ?. email ;
191+ if ( ! email || typeof email !== 'string' ) return next ( ) ;
192+ if ( await isDisposableEmail ( email ) ) {
193+ return res . status ( 403 ) . json ( { error : 'Temporary email addresses are not allowed.' , requestId : req . requestId } ) ;
194+ }
195+ next ( ) ;
196+ } catch ( err ) {
197+ console . error ( '[SECURITY] Disposable email check error:' , err . message ) ;
198+ next ( ) ;
199+ }
200+ } ;
201+ }
202+
203+ export function passwordBreachCheck ( ) {
204+ return async ( req , res , next ) => {
205+ try {
206+ if ( ! req . path . startsWith ( '/api/' ) ) return next ( ) ;
207+ const password = req . body ?. password || req . body ?. newPassword ;
208+ if ( ! password || typeof password !== 'string' ) return next ( ) ;
209+ if ( password . length < 6 ) return next ( ) ;
210+ const { breached } = await checkPasswordBreach ( password ) ;
211+ if ( breached ) {
212+ return res . status ( 400 ) . json ( {
213+ error : 'This password has been exposed in a data breach. Please choose a different password.' ,
214+ requestId : req . requestId ,
215+ } ) ;
216+ }
217+ next ( ) ;
218+ } catch ( err ) {
219+ console . error ( '[SECURITY] Password breach check error:' , err . message ) ;
220+ next ( ) ;
221+ }
222+ } ;
223+ }
224+
158225export function securityAudit ( action ) {
159226 return async ( req , res , next ) => {
160227 const ip = getClientIp ( req ) ;
161228 const origJson = res . json . bind ( res ) ;
162229 res . json = function ( body ) {
163230 if ( res . statusCode >= 400 ) {
164- console . warn ( `[AUDIT] ${ action } failed for IP ${ ip } : ${ JSON . stringify ( body ) } ` ) ;
231+ recordFailedAction ( ip , action . includes ( 'login' ) ? 'login' : 'action' ) ;
165232 }
166233 return origJson ( body ) ;
167234 } ;
0 commit comments