Skip to content

Commit c0f2dcc

Browse files
authored
Merge pull request #36 from ZeroHost-Code/beta
v1.2.0
2 parents 0bd51f3 + e9e246d commit c0f2dcc

15 files changed

Lines changed: 2162 additions & 44 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Thumbs.db
2222
# Logs
2323
logs/
2424
*.log
25+
log.txt
2526

2627
# Runtime
2728
port.txt

config/migrate.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ const tables = {
2727
{ name: 'email_change_expires', def: 'DATETIME DEFAULT NULL' },
2828
{ name: 'user_agent', def: 'VARCHAR(512) DEFAULT NULL' },
2929
{ name: 'onboarding_done', def: 'TINYINT(1) NOT NULL DEFAULT 0' },
30+
{ name: 'totp_secret', def: 'VARCHAR(255) DEFAULT NULL' },
31+
{ name: 'totp_enabled', def: 'TINYINT(1) NOT NULL DEFAULT 0' },
32+
{ name: 'recovery_codes', def: 'TEXT DEFAULT NULL' },
3033
{ name: 'created_at', def: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' },
3134
],
3235
},

middleware/security.js

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import {
2+
getClientIp,
3+
isPrivateIp,
4+
normalizeIp,
5+
isBotUserAgent,
6+
isKnownBotIp,
7+
checkHeaders,
8+
checkHoneypot,
9+
validateBrowserSignature,
10+
detectVpnProxy,
11+
checkSuspiciousQueryParams,
12+
checkBlockedCountry,
13+
checkConcurrentRequests,
14+
validateRequestTiming,
15+
} from '../services/security.js';
16+
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+
27+
const VPN_EXEMPT_PATHS = [
28+
'/api/config', '/api/health', '/api/activity',
29+
];
30+
31+
const SUSPICIOUS_IPS = new Set();
32+
33+
export function advancedBotProtection(required = false) {
34+
return async (req, res, next) => {
35+
try {
36+
if (!req.path.startsWith('/api/')) return next();
37+
const ip = getClientIp(req);
38+
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+
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+
}
47+
}
48+
49+
const { allowed: concurrentOk, count: concurrentCount } = checkConcurrentRequests(ip, 8);
50+
if (!concurrentOk && isPost) {
51+
return res.status(429).json({ error: 'Too many simultaneous requests. Slow down.', requestId: req.requestId });
52+
}
53+
54+
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 });
57+
}
58+
59+
if (isKnownBotIp(ip) && isPost) {
60+
return res.status(403).json({ error: 'Access denied', requestId: req.requestId });
61+
}
62+
63+
const { flagged: queryFlagged, param, pattern } = checkSuspiciousQueryParams(req);
64+
if (queryFlagged && isPost) {
65+
console.warn(`[SECURITY] Suspicious query param "${param}" containing "${pattern}" from IP ${ip}`);
66+
return res.status(400).json({ error: 'Invalid request parameters', requestId: req.requestId });
67+
}
68+
69+
const { triggered: honeypotTriggered, field: honeypotField } = checkHoneypot(req.body);
70+
if (honeypotTriggered && isPost) {
71+
console.warn(`[SECURITY] Honeypot triggered on field "${honeypotField}" from IP ${ip}`);
72+
return res.status(400).json({ error: 'Invalid form submission', requestId: req.requestId });
73+
}
74+
75+
next();
76+
} catch (err) {
77+
console.error('[SECURITY] Middleware error:', err.message);
78+
next();
79+
}
80+
};
81+
}
82+
83+
export function vpnProxyProtection() {
84+
return async (req, res, next) => {
85+
try {
86+
if (!req.path.startsWith('/api/')) return next();
87+
if (VPN_EXEMPT_PATHS.includes(req.path)) return next();
88+
const ip = getClientIp(req);
89+
const cleanIp = normalizeIp(ip);
90+
if (!cleanIp || isPrivateIp(cleanIp)) return next();
91+
const isPost = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method);
92+
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})`);
97+
return res.status(403).json({
98+
error: result.isTor
99+
? 'Tor network access is not allowed.'
100+
: 'VPN or proxy detected. Please disable your VPN for security reasons.',
101+
requestId: req.requestId,
102+
});
103+
}
104+
next();
105+
} catch (err) {
106+
console.error('[SECURITY] VPN proxy middleware error:', err.message);
107+
next();
108+
}
109+
};
110+
}
111+
112+
export function countryBlock() {
113+
return async (req, res, next) => {
114+
try {
115+
if (!req.path.startsWith('/api/')) return next();
116+
const isPost = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method);
117+
if (!isPost) return next();
118+
if (req.path === '/api/auth/login') return next();
119+
const ip = getClientIp(req);
120+
const { blocked, countryCode } = await checkBlockedCountry(ip);
121+
if (blocked) {
122+
console.warn(`[SECURITY] Blocked request from ${countryCode} IP ${ip}`);
123+
return res.status(403).json({ error: 'Service not available in your region.', requestId: req.requestId });
124+
}
125+
next();
126+
} catch (err) {
127+
console.error('[SECURITY] Country block middleware error:', err.message);
128+
next();
129+
}
130+
};
131+
}
132+
133+
export function browserIntegrityCheck() {
134+
return (req, res, next) => {
135+
try {
136+
if (!req.path.startsWith('/api/')) return next();
137+
const isPost = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method);
138+
if (!isPost) return next();
139+
const issues = checkHeaders(req);
140+
if (issues.length >= 3) {
141+
const ip = getClientIp(req);
142+
console.warn(`[SECURITY] Browser integrity check failed for IP ${ip}: ${issues.join(', ')}`);
143+
return res.status(403).json({ error: 'Invalid request headers. Please use a real browser.', requestId: req.requestId });
144+
}
145+
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}`);
149+
}
150+
next();
151+
} catch (err) {
152+
console.error('[SECURITY] Browser integrity error:', err.message);
153+
next();
154+
}
155+
};
156+
}
157+
158+
export function securityAudit(action) {
159+
return async (req, res, next) => {
160+
const ip = getClientIp(req);
161+
const origJson = res.json.bind(res);
162+
res.json = function (body) {
163+
if (res.statusCode >= 400) {
164+
console.warn(`[AUDIT] ${action} failed for IP ${ip}: ${JSON.stringify(body)}`);
165+
}
166+
return origJson(body);
167+
};
168+
next();
169+
};
170+
}

0 commit comments

Comments
 (0)