mdefender-pro 1.1.0
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 +194 -0
- package/config-loader.js +35 -0
- package/index.d.ts +37 -0
- package/index.js +284 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# MDefender Pro
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/mdefender)
|
|
4
|
+
[](https://github.com/mdefender/mdefender/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
**MDefender Pro** is a Web Application Firewall (WAF) middleware for Node.js/Express. It intercepts incoming HTTP requests and sends them to the MDefender Pro API for real-time threat analysis, blocking malicious traffic such as XSS, SQLi, CSRF, and other OWASP Top 10 attacks.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install mdefender
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
const express = require('express');
|
|
18
|
+
const mdefender = require('mdefender');
|
|
19
|
+
|
|
20
|
+
const app = express();
|
|
21
|
+
|
|
22
|
+
// Protect all routes
|
|
23
|
+
app.use(mdefender({
|
|
24
|
+
apiKey: 'your-api-key-here',
|
|
25
|
+
domain: 'example.com',
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
app.get('/', (req, res) => {
|
|
29
|
+
res.send('Hello, protected world!');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
app.listen(3000, () => {
|
|
33
|
+
console.log('Server running on port 3000');
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
You can configure MDefender in three ways:
|
|
40
|
+
|
|
41
|
+
1. **Inline** - Pass options directly to the middleware
|
|
42
|
+
2. **Config file** - Use `mdefender.config.js` or `mdefender.json`
|
|
43
|
+
3. **package.json** - Add a `"mdefender"` key to your `package.json`
|
|
44
|
+
|
|
45
|
+
Priority order: **Inline options > Config file > package.json**
|
|
46
|
+
|
|
47
|
+
### Options
|
|
48
|
+
|
|
49
|
+
| Option | Type | Default | Description |
|
|
50
|
+
|---|---|---|---|
|
|
51
|
+
| `apiKey` | `string` | `''` | **Required.** Your MDefender Pro API key |
|
|
52
|
+
| `domain` | `string` | `''` | **Required.** The domain to protect |
|
|
53
|
+
| `apiEndpoint` | `string` | `'https://mdefender-pro.onrender.com'` | MDefender API base URL |
|
|
54
|
+
| `mode` | `'block' \| 'monitor' \| 'off'` | `'block'` | `block` = block threats, `monitor` = log only, `off` = disabled |
|
|
55
|
+
| `blockStatusCode` | `number` | `403` | HTTP status code for blocked requests |
|
|
56
|
+
| `timeout` | `number` | `5000` | API request timeout in milliseconds |
|
|
57
|
+
| `maxBodySize` | `number` | `1048576` | Max request body size in bytes (1MB) |
|
|
58
|
+
| `logBlocked` | `boolean` | `true` | Log blocked requests to console |
|
|
59
|
+
| `customBlockPage` | `string \| null` | `null` | Path to a custom HTML file for block page |
|
|
60
|
+
| `skipPaths` | `string[]` | `['/health', '/favicon.ico']` | Paths to skip WAF checking |
|
|
61
|
+
| `skipUserAgents` | `string[]` | `[]` | User agents to skip (substring match) |
|
|
62
|
+
| `skipMethods` | `string[]` | `[]` | HTTP methods to skip entirely |
|
|
63
|
+
| `headers` | `boolean` | `true` | Forward original request headers to API |
|
|
64
|
+
| `onError` | `'allow' \| 'block'` | `'allow'` | Behavior when API is unreachable |
|
|
65
|
+
|
|
66
|
+
### Config File: `mdefender.config.js`
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
module.exports = {
|
|
70
|
+
apiKey: process.env.MDEFENDER_API_KEY,
|
|
71
|
+
domain: 'example.com',
|
|
72
|
+
mode: 'block',
|
|
73
|
+
skipPaths: ['/health', '/ping', '/favicon.ico'],
|
|
74
|
+
logBlocked: true,
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Config File: `mdefender.json`
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"apiKey": "your-api-key-here",
|
|
83
|
+
"domain": "example.com",
|
|
84
|
+
"mode": "block",
|
|
85
|
+
"skipPaths": ["/health", "/ping"],
|
|
86
|
+
"onError": "allow"
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `package.json`
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"name": "my-app",
|
|
95
|
+
"mdefender": {
|
|
96
|
+
"apiKey": "your-api-key-here",
|
|
97
|
+
"domain": "example.com"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Advanced Usage
|
|
103
|
+
|
|
104
|
+
### Custom Block Page
|
|
105
|
+
|
|
106
|
+
Provide a path to your own HTML file:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
app.use(mdefender({
|
|
110
|
+
apiKey: 'your-key',
|
|
111
|
+
domain: 'example.com',
|
|
112
|
+
customBlockPage: path.join(__dirname, 'views', 'block.html'),
|
|
113
|
+
}));
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Monitor Mode (Log Only)
|
|
117
|
+
|
|
118
|
+
Run in monitoring mode to analyze requests without blocking:
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
app.use(mdefender({
|
|
122
|
+
apiKey: 'your-key',
|
|
123
|
+
domain: 'example.com',
|
|
124
|
+
mode: 'monitor',
|
|
125
|
+
}));
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Skip Specific Paths
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
app.use(mdefender({
|
|
132
|
+
apiKey: 'your-key',
|
|
133
|
+
domain: 'example.com',
|
|
134
|
+
skipPaths: ['/health', '/api/webhook', '/static/'],
|
|
135
|
+
}));
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Block on API Error
|
|
139
|
+
|
|
140
|
+
By default, requests are allowed if the WAF API is unreachable. To block instead:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
app.use(mdefender({
|
|
144
|
+
apiKey: 'your-key',
|
|
145
|
+
domain: 'example.com',
|
|
146
|
+
onError: 'block',
|
|
147
|
+
}));
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Accessing Analysis Results
|
|
151
|
+
|
|
152
|
+
After the middleware processes a request, analysis data is attached to `req.mdefender`:
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
app.get('/dashboard', (req, res) => {
|
|
156
|
+
if (req.mdefender) {
|
|
157
|
+
console.log('Threat score:', req.mdefender.threat_score);
|
|
158
|
+
console.log('Request ID:', req.mdefender.request_id);
|
|
159
|
+
}
|
|
160
|
+
res.send('Dashboard');
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## API Reference
|
|
165
|
+
|
|
166
|
+
### `mdefender(config?)`
|
|
167
|
+
|
|
168
|
+
Returns an Express middleware function.
|
|
169
|
+
|
|
170
|
+
**Parameters:**
|
|
171
|
+
- `config` *(optional)* - `MDefenderConfig` object. Options merge with file-based config and defaults.
|
|
172
|
+
|
|
173
|
+
**Returns:** `Express middleware function`
|
|
174
|
+
|
|
175
|
+
### `mdefender.loadConfig(overrides?)`
|
|
176
|
+
|
|
177
|
+
Utility to load configuration from file sources with optional overrides.
|
|
178
|
+
|
|
179
|
+
### `mdefender.DEFAULT_CONFIG`
|
|
180
|
+
|
|
181
|
+
The default configuration object.
|
|
182
|
+
|
|
183
|
+
## How It Works
|
|
184
|
+
|
|
185
|
+
1. A request hits your Express app
|
|
186
|
+
2. MDefender intercepts it and builds a payload (method, URL, headers, body, IP, etc.)
|
|
187
|
+
3. The payload is sent to the MDefender Pro API for analysis
|
|
188
|
+
4. If the API detects a threat, it returns a `blocked` status with attack details
|
|
189
|
+
5. MDefender renders a block page and responds with the configured status code
|
|
190
|
+
6. If safe, the request continues to your route handler
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
package/config-loader.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
function findConfig(startDir) {
|
|
7
|
+
const configs = [
|
|
8
|
+
'mdefender.config.js',
|
|
9
|
+
'mdefender.config.cjs',
|
|
10
|
+
'mdefender.json',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
let dir = startDir || process.cwd();
|
|
14
|
+
|
|
15
|
+
while (dir !== path.dirname(dir)) {
|
|
16
|
+
for (const name of configs) {
|
|
17
|
+
const fp = path.join(dir, name);
|
|
18
|
+
if (fs.existsSync(fp)) return fp;
|
|
19
|
+
}
|
|
20
|
+
dir = path.dirname(dir);
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function load(startDir) {
|
|
26
|
+
const configPath = findConfig(startDir);
|
|
27
|
+
if (!configPath) return null;
|
|
28
|
+
|
|
29
|
+
if (configPath.endsWith('.json')) {
|
|
30
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
31
|
+
}
|
|
32
|
+
return require(configPath);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { findConfig, load };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
interface MDefenderConfig {
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
domain?: string;
|
|
6
|
+
apiEndpoint?: string;
|
|
7
|
+
mode?: 'block' | 'monitor' | 'off';
|
|
8
|
+
blockStatusCode?: number;
|
|
9
|
+
timeout?: number;
|
|
10
|
+
maxBodySize?: number;
|
|
11
|
+
logBlocked?: boolean;
|
|
12
|
+
customBlockPage?: string | null;
|
|
13
|
+
skipPaths?: string[];
|
|
14
|
+
skipUserAgents?: string[];
|
|
15
|
+
skipMethods?: string[];
|
|
16
|
+
headers?: boolean;
|
|
17
|
+
onError?: 'allow' | 'block';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface MDefenderRequest extends Request {
|
|
21
|
+
mdefender?: {
|
|
22
|
+
status: string;
|
|
23
|
+
threat_score: number;
|
|
24
|
+
request_id: string | null;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type MDefenderMiddleware = (req: MDefenderRequest, res: Response, next: NextFunction) => void;
|
|
29
|
+
|
|
30
|
+
declare function mdefender(config?: MDefenderConfig): MDefenderMiddleware;
|
|
31
|
+
|
|
32
|
+
declare namespace mdefender {
|
|
33
|
+
export function loadConfig(overrides?: Partial<MDefenderConfig>): MDefenderConfig;
|
|
34
|
+
export const DEFAULT_CONFIG: MDefenderConfig;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export = mdefender;
|
package/index.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const axios = require('axios');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
apiKey: '',
|
|
10
|
+
domain: '',
|
|
11
|
+
apiEndpoint: 'https://mdefender-pro.onrender.com',
|
|
12
|
+
mode: 'block', // 'block' | 'monitor' | 'off'
|
|
13
|
+
blockStatusCode: 403,
|
|
14
|
+
timeout: 5000,
|
|
15
|
+
maxBodySize: 1024 * 1024, // 1MB
|
|
16
|
+
logBlocked: true,
|
|
17
|
+
customBlockPage: null, // path to custom HTML file
|
|
18
|
+
skipPaths: ['/health', '/favicon.ico'],
|
|
19
|
+
skipUserAgents: [],
|
|
20
|
+
skipMethods: [],
|
|
21
|
+
headers: true, // forward original headers
|
|
22
|
+
onError: 'allow', // 'allow' | 'block' - what to do if API is unreachable
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function loadConfig(overrides = {}) {
|
|
26
|
+
let fileConfig = {};
|
|
27
|
+
|
|
28
|
+
// Try mdefender.config.js
|
|
29
|
+
const jsPath = path.resolve(process.cwd(), 'mdefender.config.js');
|
|
30
|
+
if (fs.existsSync(jsPath)) {
|
|
31
|
+
fileConfig = require(jsPath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Try mdefender.json
|
|
35
|
+
const jsonPath = path.resolve(process.cwd(), 'mdefender.json');
|
|
36
|
+
if (!fileConfig.apiKey && fs.existsSync(jsonPath)) {
|
|
37
|
+
fileConfig = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Try package.json "mdefender" key
|
|
41
|
+
if (!fileConfig.apiKey) {
|
|
42
|
+
try {
|
|
43
|
+
const pkg = require(path.resolve(process.cwd(), 'package.json'));
|
|
44
|
+
if (pkg.mdefender) fileConfig = pkg.mdefender;
|
|
45
|
+
} catch (e) {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { ...DEFAULT_CONFIG, ...fileConfig, ...overrides };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getClientIP(req) {
|
|
52
|
+
return req.headers['x-forwarded-for']?.split(',')[0]?.trim()
|
|
53
|
+
|| req.headers['x-real-ip']
|
|
54
|
+
|| req.connection?.remoteAddress
|
|
55
|
+
|| req.socket?.remoteAddress
|
|
56
|
+
|| 'unknown';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildPayload(req, config) {
|
|
60
|
+
const ip = getClientIP(req);
|
|
61
|
+
const originalUrl = req.originalUrl || req.url;
|
|
62
|
+
const parsedUrl = new URL(originalUrl, `http://${req.headers.host || 'localhost'}`);
|
|
63
|
+
|
|
64
|
+
const payload = {
|
|
65
|
+
domain: config.domain,
|
|
66
|
+
method: req.method,
|
|
67
|
+
url: parsedUrl.pathname,
|
|
68
|
+
query_string: parsedUrl.search || '',
|
|
69
|
+
query_params: Object.fromEntries(parsedUrl.searchParams),
|
|
70
|
+
ip: ip,
|
|
71
|
+
headers: config.headers ? req.headers : {},
|
|
72
|
+
user_agent: req.headers['user-agent'] || '',
|
|
73
|
+
referer: req.headers['referer'] || req.headers['referrer'] || '',
|
|
74
|
+
content_type: req.headers['content-type'] || '',
|
|
75
|
+
body: '',
|
|
76
|
+
body_fields: {},
|
|
77
|
+
body_field_values: '',
|
|
78
|
+
timestamp: new Date().toISOString(),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return payload;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readBody(req) {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
if (req.body) {
|
|
87
|
+
if (typeof req.body === 'string') return resolve(req.body);
|
|
88
|
+
if (typeof req.body === 'object') return resolve(JSON.stringify(req.body));
|
|
89
|
+
return resolve(String(req.body));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let body = '';
|
|
93
|
+
const maxBytes = 1024 * 1024;
|
|
94
|
+
let bytesRead = 0;
|
|
95
|
+
|
|
96
|
+
const onData = (chunk) => {
|
|
97
|
+
bytesRead += chunk.length;
|
|
98
|
+
if (bytesRead > maxBytes) {
|
|
99
|
+
req.removeListener('data', onData);
|
|
100
|
+
req.removeListener('end', onEnd);
|
|
101
|
+
resolve(body);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
body += chunk.toString();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const onEnd = () => {
|
|
108
|
+
req.removeListener('data', onData);
|
|
109
|
+
resolve(body);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
req.on('data', onData);
|
|
113
|
+
req.on('end', onEnd);
|
|
114
|
+
|
|
115
|
+
// If stream already ended
|
|
116
|
+
if (req.readableEnded || req.complete) {
|
|
117
|
+
req.removeListener('data', onData);
|
|
118
|
+
req.removeListener('end', onEnd);
|
|
119
|
+
resolve(body);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseBody(body, contentType) {
|
|
125
|
+
if (!body) return { fields: {}, values: '' };
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
if (contentType && contentType.includes('application/json')) {
|
|
129
|
+
const parsed = JSON.parse(body);
|
|
130
|
+
return { fields: parsed, values: Object.values(parsed).join(' ') };
|
|
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 };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderBlockPage(config, result) {
|
|
143
|
+
if (config.customBlockPage && fs.existsSync(config.customBlockPage)) {
|
|
144
|
+
return fs.readFileSync(config.customBlockPage, 'utf-8');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const attackType = result.attack_type || 'Unknown';
|
|
148
|
+
const confidence = result.confidence || 0;
|
|
149
|
+
const referenceId = result.reference_id || crypto.randomUUID();
|
|
150
|
+
|
|
151
|
+
return `<!DOCTYPE html>
|
|
152
|
+
<html lang="en">
|
|
153
|
+
<head>
|
|
154
|
+
<meta charset="UTF-8">
|
|
155
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
156
|
+
<title>Access Blocked - MDefender Pro</title>
|
|
157
|
+
<style>
|
|
158
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
159
|
+
body { min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
|
160
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
|
161
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
162
|
+
color: #fff; }
|
|
163
|
+
.container { text-align: center; padding: 40px; max-width: 600px; }
|
|
164
|
+
.icon { font-size: 80px; margin-bottom: 20px; opacity: 0.9; }
|
|
165
|
+
h1 { font-size: 28px; margin-bottom: 12px; font-weight: 700; }
|
|
166
|
+
.subtitle { font-size: 16px; color: rgba(255,255,255,0.7); margin-bottom: 30px; }
|
|
167
|
+
.details { background: rgba(255,255,255,0.08); border-radius: 12px; padding: 20px;
|
|
168
|
+
text-align: left; margin-bottom: 30px; backdrop-filter: blur(10px); }
|
|
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>`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function mdefender(overrides = {}) {
|
|
192
|
+
const config = loadConfig(overrides);
|
|
193
|
+
|
|
194
|
+
if (!config.apiKey) {
|
|
195
|
+
console.error('[MDefender] ERROR: No API key provided. Set it in mdefender.config.js or pass it as an option.');
|
|
196
|
+
return (req, res, next) => next();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (config.mode === 'off') {
|
|
200
|
+
return (req, res, next) => next();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const client = axios.create({
|
|
204
|
+
baseURL: config.apiEndpoint,
|
|
205
|
+
timeout: config.timeout,
|
|
206
|
+
headers: {
|
|
207
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
208
|
+
'Content-Type': 'application/json',
|
|
209
|
+
'X-MDefender-Version': '1.0.0',
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return async function mdefenderMiddleware(req, res, next) {
|
|
214
|
+
// Skip certain paths
|
|
215
|
+
if (config.skipPaths.some(p => req.url.startsWith(p))) return next();
|
|
216
|
+
|
|
217
|
+
// Skip certain methods
|
|
218
|
+
if (config.skipMethods.includes(req.method)) return next();
|
|
219
|
+
|
|
220
|
+
// Skip certain user agents
|
|
221
|
+
const ua = req.headers['user-agent'] || '';
|
|
222
|
+
if (config.skipUserAgents.some(s => ua.toLowerCase().includes(s.toLowerCase()))) return next();
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
// Read and parse request body
|
|
226
|
+
const rawBody = await readBody(req);
|
|
227
|
+
const contentType = req.headers['content-type'] || '';
|
|
228
|
+
const { fields, values } = parseBody(rawBody, contentType);
|
|
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;
|
|
243
|
+
|
|
244
|
+
if (result.status === 'blocked') {
|
|
245
|
+
if (config.logBlocked) {
|
|
246
|
+
console.log(`[MDefender] BLOCKED ${req.method} ${req.url} - ${result.attack_type} (${(result.confidence * 100).toFixed(1)}%)`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const blockPage = result.block_page || renderBlockPage(config, result);
|
|
250
|
+
res.writeHead(config.blockStatusCode, {
|
|
251
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
252
|
+
'X-MDefender-Status': 'blocked',
|
|
253
|
+
'X-MDefender-Attack-Type': result.attack_type || 'unknown',
|
|
254
|
+
});
|
|
255
|
+
return res.end(blockPage);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Request is safe - attach result to request for downstream use
|
|
259
|
+
req.mdefender = {
|
|
260
|
+
status: 'allowed',
|
|
261
|
+
threat_score: result.threat_score || 0,
|
|
262
|
+
request_id: result.request_id || null,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
return next();
|
|
266
|
+
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error(`[MDefender] API Error: ${error.message}`);
|
|
269
|
+
|
|
270
|
+
if (config.onError === 'block') {
|
|
271
|
+
res.writeHead(503, { 'Content-Type': 'text/html' });
|
|
272
|
+
return res.end('<html><body><h1>Service Temporarily Unavailable</h1><p>WAF service is currently unreachable. Please try again later.</p></body></html>');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Default: allow request on error
|
|
276
|
+
return next();
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
mdefender.loadConfig = loadConfig;
|
|
282
|
+
mdefender.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
|
283
|
+
|
|
284
|
+
module.exports = mdefender;
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mdefender-pro",
|
|
3
|
+
"version": "1.1.0",
|
|
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
|
+
"dependencies": {
|
|
27
|
+
"axios": "^1.6.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"express": ">=4.0.0"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"index.js",
|
|
34
|
+
"index.d.ts",
|
|
35
|
+
"README.md",
|
|
36
|
+
"config-loader.js"
|
|
37
|
+
],
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/mdefender/mdefender"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://mdefender-pro-6e3r.onrender.com",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/mdefender/mdefender/issues"
|
|
45
|
+
}
|
|
46
|
+
}
|