mdefender-pro 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -136
- package/bin/mdefender.js +80 -0
- package/block-page.html +541 -0
- package/client.mjs +446 -0
- package/config-loader.js +35 -35
- package/index.d.ts +37 -37
- package/index.js +228 -186
- package/package.json +57 -46
- package/vite.js +18 -0
- package/vite.mjs +17 -0
package/index.js
CHANGED
|
@@ -1,40 +1,84 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const https = require('https');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const fs = require('fs');
|
|
6
7
|
const crypto = require('crypto');
|
|
8
|
+
const { URL } = require('url');
|
|
9
|
+
|
|
10
|
+
const httpAgent = new http.Agent({ keepAlive: true, timeout: 5000 });
|
|
11
|
+
const httpsAgent = new https.Agent({ keepAlive: true, timeout: 5000 });
|
|
12
|
+
|
|
13
|
+
// In-memory cache for bundled block page with mtime check
|
|
14
|
+
let cachedDefaultTemplate = null;
|
|
15
|
+
let cachedTemplateMtime = 0;
|
|
16
|
+
|
|
17
|
+
function getDefaultTemplate() {
|
|
18
|
+
try {
|
|
19
|
+
const templatePath = path.join(__dirname, 'block-page.html');
|
|
20
|
+
if (fs.existsSync(templatePath)) {
|
|
21
|
+
const stats = fs.statSync(templatePath);
|
|
22
|
+
if (cachedDefaultTemplate === null || stats.mtimeMs > cachedTemplateMtime) {
|
|
23
|
+
cachedDefaultTemplate = fs.readFileSync(templatePath, 'utf8');
|
|
24
|
+
cachedTemplateMtime = stats.mtimeMs;
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
cachedDefaultTemplate = '';
|
|
28
|
+
}
|
|
29
|
+
} catch (e) {
|
|
30
|
+
if (!cachedDefaultTemplate) cachedDefaultTemplate = '';
|
|
31
|
+
}
|
|
32
|
+
return cachedDefaultTemplate;
|
|
33
|
+
}
|
|
7
34
|
|
|
8
35
|
const DEFAULT_CONFIG = {
|
|
9
36
|
apiKey: '',
|
|
10
37
|
domain: '',
|
|
11
|
-
apiEndpoint: '
|
|
38
|
+
apiEndpoint: 'http://127.0.0.1:8000',
|
|
12
39
|
mode: 'block', // 'block' | 'monitor' | 'off'
|
|
13
40
|
blockStatusCode: 403,
|
|
14
41
|
timeout: 5000,
|
|
15
42
|
maxBodySize: 1024 * 1024, // 1MB
|
|
16
43
|
logBlocked: true,
|
|
17
|
-
customBlockPage: null,
|
|
44
|
+
customBlockPage: null,
|
|
18
45
|
skipPaths: ['/health', '/favicon.ico'],
|
|
19
46
|
skipUserAgents: [],
|
|
20
47
|
skipMethods: [],
|
|
21
|
-
headers: true,
|
|
22
|
-
onError: 'allow',
|
|
48
|
+
headers: true,
|
|
49
|
+
onError: 'allow', // 'allow' | 'block'
|
|
23
50
|
};
|
|
24
51
|
|
|
25
52
|
function loadConfig(overrides = {}) {
|
|
26
53
|
let fileConfig = {};
|
|
27
54
|
|
|
55
|
+
// Try config-loader
|
|
56
|
+
try {
|
|
57
|
+
const configLoader = require('./config-loader');
|
|
58
|
+
const loaded = configLoader.load();
|
|
59
|
+
if (loaded) fileConfig = loaded;
|
|
60
|
+
} catch (e) {}
|
|
61
|
+
|
|
28
62
|
// Try mdefender.config.js
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
63
|
+
if (!fileConfig.apiKey) {
|
|
64
|
+
const jsPath = path.resolve(process.cwd(), 'mdefender.config.js');
|
|
65
|
+
if (fs.existsSync(jsPath)) {
|
|
66
|
+
try {
|
|
67
|
+
fileConfig = require(jsPath);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.warn('[MDefender] Failed reading mdefender.config.js:', e.message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
32
72
|
}
|
|
33
73
|
|
|
34
74
|
// Try mdefender.json
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
75
|
+
if (!fileConfig.apiKey) {
|
|
76
|
+
const jsonPath = path.resolve(process.cwd(), 'mdefender.json');
|
|
77
|
+
if (fs.existsSync(jsonPath)) {
|
|
78
|
+
try {
|
|
79
|
+
fileConfig = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
|
|
80
|
+
} catch (e) {}
|
|
81
|
+
}
|
|
38
82
|
}
|
|
39
83
|
|
|
40
84
|
// Try package.json "mdefender" key
|
|
@@ -45,154 +89,154 @@ function loadConfig(overrides = {}) {
|
|
|
45
89
|
} catch (e) {}
|
|
46
90
|
}
|
|
47
91
|
|
|
48
|
-
|
|
92
|
+
// Environment variables
|
|
93
|
+
const envConfig = {};
|
|
94
|
+
if (process.env.MDEFENDER_API_KEY) envConfig.apiKey = process.env.MDEFENDER_API_KEY;
|
|
95
|
+
if (process.env.MDEFENDER_DOMAIN) envConfig.domain = process.env.MDEFENDER_DOMAIN;
|
|
96
|
+
if (process.env.MDEFENDER_API_ENDPOINT || process.env.MDEFENDER_ENDPOINT) {
|
|
97
|
+
envConfig.apiEndpoint = process.env.MDEFENDER_API_ENDPOINT || process.env.MDEFENDER_ENDPOINT;
|
|
98
|
+
}
|
|
99
|
+
if (process.env.MDEFENDER_MODE) envConfig.mode = process.env.MDEFENDER_MODE;
|
|
100
|
+
|
|
101
|
+
return { ...DEFAULT_CONFIG, ...fileConfig, ...envConfig, ...overrides };
|
|
49
102
|
}
|
|
50
103
|
|
|
51
104
|
function getClientIP(req) {
|
|
52
|
-
|
|
53
|
-
|
|
105
|
+
const forwarded = req.headers['x-forwarded-for'];
|
|
106
|
+
if (forwarded) {
|
|
107
|
+
return forwarded.split(',')[0].trim();
|
|
108
|
+
}
|
|
109
|
+
return req.headers['x-real-ip']
|
|
54
110
|
|| req.connection?.remoteAddress
|
|
55
111
|
|| req.socket?.remoteAddress
|
|
56
|
-
|| '
|
|
112
|
+
|| '127.0.0.1';
|
|
57
113
|
}
|
|
58
114
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
body_field_values: '',
|
|
78
|
-
timestamp: new Date().toISOString(),
|
|
79
|
-
};
|
|
115
|
+
function extractBody(req) {
|
|
116
|
+
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
|
|
117
|
+
return { raw: '', fields: {}, values: '' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (req.body) {
|
|
121
|
+
if (typeof req.body === 'object') {
|
|
122
|
+
try {
|
|
123
|
+
const raw = JSON.stringify(req.body);
|
|
124
|
+
const values = Object.values(req.body).map(v => typeof v === 'object' ? JSON.stringify(v) : String(v)).join(' ');
|
|
125
|
+
return { raw, fields: req.body, values };
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return { raw: '', fields: {}, values: '' };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const str = String(req.body);
|
|
131
|
+
return { raw: str, fields: {}, values: str };
|
|
132
|
+
}
|
|
80
133
|
|
|
81
|
-
return
|
|
134
|
+
return { raw: '', fields: {}, values: '' };
|
|
82
135
|
}
|
|
83
136
|
|
|
84
|
-
function
|
|
85
|
-
return new Promise((resolve) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
137
|
+
function sendAnalyzeRequest(endpointUrl, apiKey, data, timeoutMs = 5000) {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
try {
|
|
140
|
+
const parsed = new URL('/api/analyze', endpointUrl);
|
|
141
|
+
const postData = JSON.stringify(data);
|
|
142
|
+
const isHttps = parsed.protocol === 'https:';
|
|
143
|
+
const transport = isHttps ? https : http;
|
|
144
|
+
const agent = isHttps ? httpsAgent : httpAgent;
|
|
91
145
|
|
|
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
|
-
|
|
146
|
+
const options = {
|
|
147
|
+
hostname: parsed.hostname,
|
|
148
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
149
|
+
path: parsed.pathname,
|
|
150
|
+
method: 'POST',
|
|
151
|
+
agent: agent,
|
|
152
|
+
timeout: timeoutMs,
|
|
153
|
+
headers: {
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
156
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
157
|
+
'X-MDefender-Version': '1.1.0'
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const req = transport.request(options, (res) => {
|
|
162
|
+
let body = '';
|
|
163
|
+
res.setEncoding('utf8');
|
|
164
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
165
|
+
res.on('end', () => {
|
|
166
|
+
try {
|
|
167
|
+
const json = JSON.parse(body);
|
|
168
|
+
resolve({ statusCode: res.statusCode, data: json });
|
|
169
|
+
} catch (e) {
|
|
170
|
+
resolve({ statusCode: res.statusCode, data: { status: res.statusCode === 200 ? 'allowed' : 'error', raw: body } });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
req.on('error', (err) => {
|
|
176
|
+
reject(err);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
req.on('timeout', () => {
|
|
180
|
+
req.destroy(new Error(`WAF request timed out after ${timeoutMs}ms`));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
req.write(postData);
|
|
184
|
+
req.end();
|
|
185
|
+
} catch (err) {
|
|
186
|
+
reject(err);
|
|
120
187
|
}
|
|
121
188
|
});
|
|
122
189
|
}
|
|
123
190
|
|
|
124
|
-
function
|
|
125
|
-
if (!
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (contentType && contentType.includes('application/x-www-form-urlencoded')) {
|
|
133
|
-
const params = new URLSearchParams(body);
|
|
134
|
-
const fields = Object.fromEntries(params);
|
|
135
|
-
return { fields, values: Object.values(fields).join(' ') };
|
|
136
|
-
}
|
|
137
|
-
} catch (e) {}
|
|
138
|
-
|
|
139
|
-
return { fields: {}, values: body };
|
|
191
|
+
function escapeHtml(str) {
|
|
192
|
+
if (!str) return '';
|
|
193
|
+
return String(str)
|
|
194
|
+
.replace(/&/g, '&')
|
|
195
|
+
.replace(/</g, '<')
|
|
196
|
+
.replace(/>/g, '>')
|
|
197
|
+
.replace(/"/g, '"')
|
|
198
|
+
.replace(/'/g, ''');
|
|
140
199
|
}
|
|
141
200
|
|
|
142
|
-
function renderBlockPage(config, result) {
|
|
143
|
-
|
|
144
|
-
|
|
201
|
+
function renderBlockPage(config, result, req) {
|
|
202
|
+
let template = '';
|
|
203
|
+
if (config.customBlockPage) {
|
|
204
|
+
try {
|
|
205
|
+
if (fs.existsSync(config.customBlockPage)) {
|
|
206
|
+
template = fs.readFileSync(config.customBlockPage, 'utf-8');
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {}
|
|
145
209
|
}
|
|
146
210
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
body
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
.
|
|
164
|
-
.
|
|
165
|
-
|
|
166
|
-
.
|
|
167
|
-
.
|
|
168
|
-
|
|
169
|
-
.details p { font-size: 14px; color: rgba(255,255,255,0.8); margin: 8px 0; }
|
|
170
|
-
.details strong { color: #e74c3c; }
|
|
171
|
-
.footer { font-size: 12px; color: rgba(255,255,255,0.4); }
|
|
172
|
-
</style>
|
|
173
|
-
</head>
|
|
174
|
-
<body>
|
|
175
|
-
<div class="container">
|
|
176
|
-
<div class="icon">🚫</div>
|
|
177
|
-
<h1>Access Blocked</h1>
|
|
178
|
-
<p class="subtitle">This request has been blocked by MDefender Pro WAF</p>
|
|
179
|
-
<div class="details">
|
|
180
|
-
<p><strong>Attack Type:</strong> ${attackType}</p>
|
|
181
|
-
<p><strong>Confidence:</strong> ${(confidence * 100).toFixed(1)}%</p>
|
|
182
|
-
<p><strong>Reference ID:</strong> ${referenceId}</p>
|
|
183
|
-
<p><strong>Time:</strong> ${new Date().toISOString()}</p>
|
|
184
|
-
</div>
|
|
185
|
-
<p class="footer">MDefender Pro - Web Application Firewall</p>
|
|
186
|
-
</div>
|
|
187
|
-
</body>
|
|
188
|
-
</html>`;
|
|
211
|
+
if (!template) {
|
|
212
|
+
template = getDefaultTemplate();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const attackType = result.attack_type || 'Malicious Payload Detected';
|
|
216
|
+
const clientIp = getClientIP(req);
|
|
217
|
+
const reason = result.reason || result.message || 'Request blocked by MDefender Pro WAF security policies.';
|
|
218
|
+
const referenceId = result.reference_id || ('MDF-' + crypto.randomBytes(4).toString('hex').toUpperCase());
|
|
219
|
+
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
220
|
+
const siteName = config.domain || req.headers.host || 'Protected Website';
|
|
221
|
+
|
|
222
|
+
if (!template) {
|
|
223
|
+
return `<!DOCTYPE html><html><head><title>403 Access Denied</title></head><body style="background:#060913;color:#fff;font-family:sans-serif;padding:40px;text-align:center"><h1>403 Forbidden</h1><p>${escapeHtml(reason)}</p><p>Incident ID: ${escapeHtml(referenceId)}</p></body></html>`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return template
|
|
227
|
+
.replace(/\{\{(REFERENCE_ID|reference_id)\}\}/g, escapeHtml(referenceId))
|
|
228
|
+
.replace(/\{\{(ATTACK_TYPE|attack_type)\}\}/g, escapeHtml(attackType))
|
|
229
|
+
.replace(/\{\{(CLIENT_IP|client_ip)\}\}/g, escapeHtml(clientIp))
|
|
230
|
+
.replace(/\{\{(SITE_NAME|site_name|website_name)\}\}/g, escapeHtml(siteName))
|
|
231
|
+
.replace(/\{\{(TIMESTAMP|timestamp)\}\}/g, escapeHtml(timestamp))
|
|
232
|
+
.replace(/\{\{(REASON|reason)\}\}/g, escapeHtml(reason));
|
|
189
233
|
}
|
|
190
234
|
|
|
191
235
|
function mdefender(overrides = {}) {
|
|
192
236
|
const config = loadConfig(overrides);
|
|
193
237
|
|
|
194
238
|
if (!config.apiKey) {
|
|
195
|
-
console.error('[MDefender]
|
|
239
|
+
console.error('[MDefender] WARNING: No API key provided. Running in bypass mode.');
|
|
196
240
|
return (req, res, next) => next();
|
|
197
241
|
}
|
|
198
242
|
|
|
@@ -200,92 +244,89 @@ function mdefender(overrides = {}) {
|
|
|
200
244
|
return (req, res, next) => next();
|
|
201
245
|
}
|
|
202
246
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
'Authorization': `Bearer ${config.apiKey}`,
|
|
208
|
-
'Content-Type': 'application/json',
|
|
209
|
-
'X-MDefender-Version': '1.2.0',
|
|
210
|
-
},
|
|
211
|
-
});
|
|
247
|
+
console.log(`[MDefender] WAF active & protecting domain: "${config.domain || 'default'}" via ${config.apiEndpoint}`);
|
|
248
|
+
|
|
249
|
+
// Warm-up in-memory block page template cache on initialization
|
|
250
|
+
getDefaultTemplate();
|
|
212
251
|
|
|
213
252
|
return async function mdefenderMiddleware(req, res, next) {
|
|
214
|
-
// Skip
|
|
215
|
-
|
|
253
|
+
// 1. Skip paths
|
|
254
|
+
const urlPath = (req.originalUrl || req.url || '').split('?')[0];
|
|
255
|
+
if (config.skipPaths.some(p => urlPath.startsWith(p))) return next();
|
|
216
256
|
|
|
217
|
-
// Skip
|
|
257
|
+
// 2. Skip methods
|
|
218
258
|
if (config.skipMethods.includes(req.method)) return next();
|
|
219
259
|
|
|
220
|
-
// Skip
|
|
260
|
+
// 3. Skip user agents
|
|
221
261
|
const ua = req.headers['user-agent'] || '';
|
|
222
262
|
if (config.skipUserAgents.some(s => ua.toLowerCase().includes(s.toLowerCase()))) return next();
|
|
223
263
|
|
|
224
264
|
try {
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
// Build payload
|
|
231
|
-
const payload = buildPayload(req, config);
|
|
232
|
-
payload.body = rawBody;
|
|
233
|
-
payload.body_fields = fields;
|
|
234
|
-
payload.body_field_values = values;
|
|
235
|
-
|
|
236
|
-
// Send to MDefender API
|
|
237
|
-
const response = await client.post('/api/analyze', {
|
|
238
|
-
request: payload,
|
|
239
|
-
domain: config.domain,
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
const result = response.data;
|
|
265
|
+
const clientIp = getClientIP(req);
|
|
266
|
+
const host = req.headers.host || 'localhost';
|
|
267
|
+
const parsedUrl = new URL(req.originalUrl || req.url, `http://${host}`);
|
|
268
|
+
const bodyInfo = extractBody(req);
|
|
243
269
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
270
|
+
const payload = {
|
|
271
|
+
domain: config.domain || host.split(':')[0],
|
|
272
|
+
request: {
|
|
273
|
+
method: req.method,
|
|
274
|
+
url: parsedUrl.pathname,
|
|
275
|
+
query_string: parsedUrl.search || '',
|
|
276
|
+
query_params: Object.fromEntries(parsedUrl.searchParams),
|
|
277
|
+
ip: clientIp,
|
|
278
|
+
headers: req.headers || {},
|
|
279
|
+
user_agent: ua,
|
|
280
|
+
referer: req.headers['referer'] || req.headers['referrer'] || '',
|
|
281
|
+
content_type: req.headers['content-type'] || '',
|
|
282
|
+
body: bodyInfo.raw,
|
|
283
|
+
body_fields: bodyInfo.fields,
|
|
284
|
+
body_field_values: bodyInfo.values,
|
|
285
|
+
timestamp: new Date().toISOString(),
|
|
255
286
|
}
|
|
287
|
+
};
|
|
256
288
|
|
|
257
|
-
|
|
289
|
+
const resp = await sendAnalyzeRequest(config.apiEndpoint, config.apiKey, payload, config.timeout);
|
|
290
|
+
const result = resp.data;
|
|
291
|
+
|
|
292
|
+
if (result.status === 'blocked') {
|
|
258
293
|
if (config.logBlocked) {
|
|
259
|
-
console.
|
|
294
|
+
console.warn(`[MDefender] BLOCKED ${req.method} ${parsedUrl.pathname} - ${result.attack_type || 'Malicious Payload'} (${((result.confidence || 0.95) * 100).toFixed(0)}%) [IP: ${clientIp}]`);
|
|
260
295
|
}
|
|
261
296
|
|
|
262
|
-
const blockPage = result.block_page || renderBlockPage(config, result);
|
|
263
|
-
|
|
297
|
+
const blockPage = result.block_page || renderBlockPage(config, result, req);
|
|
298
|
+
const headers = {
|
|
264
299
|
'Content-Type': 'text/html; charset=utf-8',
|
|
265
300
|
'X-MDefender-Status': 'blocked',
|
|
266
301
|
'X-MDefender-Attack-Type': result.attack_type || 'unknown',
|
|
267
|
-
|
|
302
|
+
'X-MDefender-Ref': result.reference_id || 'MDF-BLOCKED',
|
|
303
|
+
};
|
|
304
|
+
if (req.headers.origin) {
|
|
305
|
+
headers['Access-Control-Allow-Origin'] = req.headers.origin;
|
|
306
|
+
headers['Access-Control-Allow-Credentials'] = 'true';
|
|
307
|
+
}
|
|
308
|
+
res.writeHead(config.blockStatusCode || 403, headers);
|
|
268
309
|
return res.end(blockPage);
|
|
269
310
|
}
|
|
270
311
|
|
|
271
|
-
//
|
|
312
|
+
// Safe request
|
|
272
313
|
req.mdefender = {
|
|
273
314
|
status: 'allowed',
|
|
274
315
|
threat_score: result.threat_score || 0,
|
|
275
|
-
|
|
316
|
+
reference_id: result.reference_id || null,
|
|
276
317
|
};
|
|
277
318
|
|
|
278
319
|
return next();
|
|
279
320
|
|
|
280
321
|
} catch (error) {
|
|
281
|
-
console.error(`[MDefender]
|
|
322
|
+
console.error(`[MDefender] WAF Warning: ${error.message}`);
|
|
282
323
|
|
|
283
324
|
if (config.onError === 'block') {
|
|
284
325
|
res.writeHead(503, { 'Content-Type': 'text/html' });
|
|
285
|
-
return res.end('<
|
|
326
|
+
return res.end('<h1>503 Service Unavailable</h1><p>WAF security service unreachable.</p>');
|
|
286
327
|
}
|
|
287
328
|
|
|
288
|
-
// Default: allow
|
|
329
|
+
// Default: Fail-safe allow legitimate traffic to continue
|
|
289
330
|
return next();
|
|
290
331
|
}
|
|
291
332
|
};
|
|
@@ -293,5 +334,6 @@ function mdefender(overrides = {}) {
|
|
|
293
334
|
|
|
294
335
|
mdefender.loadConfig = loadConfig;
|
|
295
336
|
mdefender.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
|
337
|
+
mdefender.sendAnalyzeRequest = sendAnalyzeRequest;
|
|
296
338
|
|
|
297
339
|
module.exports = mdefender;
|
package/package.json
CHANGED
|
@@ -1,46 +1,57 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mdefender-pro",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "MDefender Pro - Web Application Firewall middleware for Node.js/Express. Protects against XSS, SQLi, CSRF, and other OWASP Top 10 attacks.",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"types": "index.d.ts",
|
|
7
|
-
"keywords": [
|
|
8
|
-
"waf",
|
|
9
|
-
"web-application-firewall",
|
|
10
|
-
"security",
|
|
11
|
-
"firewall",
|
|
12
|
-
"xss",
|
|
13
|
-
"sqli",
|
|
14
|
-
"csrf",
|
|
15
|
-
"protection",
|
|
16
|
-
"middleware",
|
|
17
|
-
"express",
|
|
18
|
-
"nodejs",
|
|
19
|
-
"owasp"
|
|
20
|
-
],
|
|
21
|
-
"author": "MDefender Pro",
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"engines": {
|
|
24
|
-
"node": ">=14.0.0"
|
|
25
|
-
},
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
},
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"index.
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "mdefender-pro",
|
|
3
|
+
"version": "1.2.2",
|
|
4
|
+
"description": "MDefender Pro - Web Application Firewall middleware for Node.js/Express. Protects against XSS, SQLi, CSRF, and other OWASP Top 10 attacks.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"waf",
|
|
9
|
+
"web-application-firewall",
|
|
10
|
+
"security",
|
|
11
|
+
"firewall",
|
|
12
|
+
"xss",
|
|
13
|
+
"sqli",
|
|
14
|
+
"csrf",
|
|
15
|
+
"protection",
|
|
16
|
+
"middleware",
|
|
17
|
+
"express",
|
|
18
|
+
"nodejs",
|
|
19
|
+
"owasp"
|
|
20
|
+
],
|
|
21
|
+
"author": "MDefender Pro",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=14.0.0"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"mdefender-pro": "bin/mdefender.js"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"express": "^4.18.0 || ^5.0.0"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"index.js",
|
|
35
|
+
"index.d.ts",
|
|
36
|
+
"README.md",
|
|
37
|
+
"config-loader.js",
|
|
38
|
+
"client.mjs",
|
|
39
|
+
"vite.js",
|
|
40
|
+
"vite.mjs",
|
|
41
|
+
"block-page.html",
|
|
42
|
+
"bin"
|
|
43
|
+
],
|
|
44
|
+
"exports": {
|
|
45
|
+
".": "./index.js",
|
|
46
|
+
"./client": "./client.mjs",
|
|
47
|
+
"./vite": "./vite.mjs"
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/mdefender/mdefender.git"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://mdefender-pro-6e3r.onrender.com",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/mdefender/mdefender/issues"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/vite.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// vite.js - Official MDefender Pro Vite Plugin (CJS)
|
|
2
|
+
const mdefender = require('./index.js');
|
|
3
|
+
|
|
4
|
+
function mdefenderVite(options = {}) {
|
|
5
|
+
const middleware = mdefender(options);
|
|
6
|
+
return {
|
|
7
|
+
name: 'mdefender-vite-plugin',
|
|
8
|
+
configureServer(server) {
|
|
9
|
+
server.middlewares.use(middleware);
|
|
10
|
+
},
|
|
11
|
+
configurePreviewServer(server) {
|
|
12
|
+
server.middlewares.use(middleware);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { mdefenderVite };
|
|
18
|
+
module.exports.default = mdefenderVite;
|