Skip to content

Commit 1ac6f67

Browse files
committed
fix: replace req.path with req.originalUrl in security middleware to prevent Express mount prefix bypass
1 parent 2f78758 commit 1ac6f67

2 files changed

Lines changed: 11 additions & 11 deletions

File tree

middleware/security.js

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ const SENSITIVE_PATHS = [
3535
export function advancedBotProtection() {
3636
return async (req, res, next) => {
3737
try {
38-
if (!req.path.startsWith('/api/')) return next();
3938
const ip = getClientIp(req);
4039
const ua = (req.headers['user-agent'] || '').toString().slice(0, 512);
4140
const isPost = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method);
@@ -76,7 +75,9 @@ export function advancedBotProtection() {
7675
return res.status(400).json({ error: 'Request contains invalid content', requestId: req.requestId });
7776
}
7877

79-
if (isPost && SENSITIVE_PATHS.includes(req.path)) {
78+
const fullPath = req.originalUrl || req.baseUrl + req.path;
79+
80+
if (isPost && SENSITIVE_PATHS.includes(fullPath)) {
8081
const referrerCheck = checkReferrer(req);
8182
if (!referrerCheck.passed) {
8283
recordFailedAction(ip);
@@ -103,16 +104,16 @@ export function advancedBotProtection() {
103104
export function vpnProxyProtection() {
104105
return async (req, res, next) => {
105106
try {
106-
if (!req.path.startsWith('/api/')) return next();
107-
if (VPN_EXEMPT_PATHS.includes(req.path)) return next();
107+
const fullPath = req.originalUrl || req.baseUrl + req.path;
108+
if (VPN_EXEMPT_PATHS.includes(fullPath)) return next();
108109
const ip = getClientIp(req);
109110
const cleanIp = normalizeIp(ip);
110111
if (!cleanIp || isPrivateIp(cleanIp)) return next();
111112
const isPost = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method);
112113
if (!isPost) return next();
113114
const [vpnResult, blacklistResult] = await Promise.all([
114115
detectVpnProxy(cleanIp),
115-
req.path.startsWith('/api/auth/') || req.path.startsWith('/api/servers/') ? checkIpBlacklists(cleanIp) : { listed: false },
116+
fullPath.startsWith('/api/auth/') || fullPath.startsWith('/api/servers/') ? checkIpBlacklists(cleanIp) : { listed: false },
116117
]);
117118
if (vpnResult.isVpn || vpnResult.isProxy || vpnResult.isTor) {
118119
recordFailedAction(ip);
@@ -141,10 +142,10 @@ export function vpnProxyProtection() {
141142
export function countryBlock() {
142143
return async (req, res, next) => {
143144
try {
144-
if (!req.path.startsWith('/api/')) return next();
145+
const fullPath = req.originalUrl || req.baseUrl + req.path;
145146
const isPost = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method);
146147
if (!isPost) return next();
147-
if (req.path === '/api/auth/login') return next();
148+
if (fullPath === '/api/auth/login') return next();
148149
const ip = getClientIp(req);
149150
const { blocked, countryCode } = await checkBlockedCountry(ip);
150151
if (blocked) {
@@ -162,7 +163,6 @@ export function countryBlock() {
162163
export function browserIntegrityCheck() {
163164
return (req, res, next) => {
164165
try {
165-
if (!req.path.startsWith('/api/')) return next();
166166
const isPost = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method);
167167
if (!isPost) return next();
168168
const issues = checkHeaders(req);
@@ -186,7 +186,6 @@ export function browserIntegrityCheck() {
186186
export function disposableEmailCheck() {
187187
return async (req, res, next) => {
188188
try {
189-
if (!req.path.startsWith('/api/')) return next();
190189
const email = req.body?.email;
191190
if (!email || typeof email !== 'string') return next();
192191
if (await isDisposableEmail(email)) {
@@ -203,7 +202,7 @@ export function disposableEmailCheck() {
203202
export function passwordBreachCheck() {
204203
return async (req, res, next) => {
205204
try {
206-
if (!req.path.startsWith('/api/')) return next();
205+
if (!req.originalUrl.startsWith('/api/')) return next();
207206
const password = req.body?.password || req.body?.newPassword;
208207
if (!password || typeof password !== 'string') return next();
209208
if (password.length < 6) return next();

services/security.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,8 @@ export function calculateOverallRisk(req) {
314314
if (!encoding) { risk += 5; reasons.push('no_encoding'); }
315315
const referer = req.headers['referer'] || '';
316316
if (!referer && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
317-
if (req.path.startsWith('/api/') && !req.path.includes('/auth/login') && !req.path.includes('/auth/register')) {
317+
const fullPath = req.originalUrl || req.baseUrl + req.path;
318+
if (fullPath.startsWith('/api/') && !fullPath.includes('/auth/login') && !fullPath.includes('/auth/register')) {
318319
risk += 10; reasons.push('no_referrer');
319320
}
320321
}

0 commit comments

Comments
 (0)