Skip to content

Commit b9dbb5e

Browse files
authored
Merge pull request #38 from ZeroHost-Code/beta
v1.2.1
2 parents c0f2dcc + 070bbf2 commit b9dbb5e

5 files changed

Lines changed: 392 additions & 43 deletions

File tree

middleware/security.js

Lines changed: 107 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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-
2725
const 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+
158225
export 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
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zerohost-dashboard",
3-
"version": "1.2.0",
3+
"version": "1.2.1",
44
"private": true,
55
"type": "module",
66
"scripts": {

public/js/app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1654,7 +1654,7 @@ async function renderDashboard() {
16541654
<div style="padding:8px 12px 0;display:flex;gap:16px;justify-content:center;flex-wrap:wrap">
16551655
16561656
</div>
1657-
<div style="padding:4px 0 8px;text-align:center;font-size:0.7rem;color:var(--text-muted);letter-spacing:0.05em">v1.2.0</div>
1657+
<div style="padding:4px 0 8px;text-align:center;font-size:0.7rem;color:var(--text-muted);letter-spacing:0.05em">v1.2.1</div>
16581658
</div>
16591659
<div class="sidebar-resizer" id="sidebar-resizer"></div>
16601660
</aside>

server.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { query, closePool, getPoolStatus } from './config/db.js';
3838
import { getRecentActivity } from './services/activity.js';
3939
import { authenticateToken } from './middleware/auth.js';
4040
import { ensureLogFile, writeLog, startLogCleaner } from './services/fileLogger.js';
41-
import { advancedBotProtection, vpnProxyProtection, countryBlock, browserIntegrityCheck } from './middleware/security.js';
41+
import { advancedBotProtection, vpnProxyProtection, countryBlock, browserIntegrityCheck, disposableEmailCheck, passwordBreachCheck } from './middleware/security.js';
4242

4343
const app = express();
4444

@@ -285,6 +285,9 @@ app.get('/api/config', (req, res) => {
285285
});
286286
});
287287

288+
app.use('/api/auth/register', disposableEmailCheck());
289+
app.use('/api/auth/register', passwordBreachCheck());
290+
app.use('/api/auth/change-password', passwordBreachCheck());
288291
app.use('/api/auth', authRoutes);
289292
app.use('/api/auth', passkeyRoutes);
290293
app.use('/api/auth', totpRoutes);

0 commit comments

Comments
 (0)