securenow 5.8.0 → 5.9.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/CONSUMING-APPS-GUIDE.md +8 -0
- package/NPM_README.md +22 -1
- package/README.md +3 -0
- package/cli/auth.js +1 -1
- package/cli/config.js +6 -0
- package/docs/ENVIRONMENT-VARIABLES.md +47 -2
- package/docs/EXPRESS-BODY-CAPTURE.md +3 -2
- package/docs/REQUEST-BODY-CAPTURE.md +15 -6
- package/free-trial-banner.js +7 -1
- package/nextjs-auto-capture.js +1 -1
- package/nextjs-middleware.js +8 -3
- package/nextjs-wrapper.js +1 -1
- package/package.json +1 -1
- package/tracing.js +14 -10
- package/web-vite.mjs +15 -5
package/CONSUMING-APPS-GUIDE.md
CHANGED
|
@@ -362,6 +362,14 @@ OTEL_EXPORTER_OTLP_HEADERS="x-api-key=KEY"
|
|
|
362
362
|
SECURENOW_APPID=my-app # Your app name
|
|
363
363
|
OTEL_SERVICE_NAME=my-app # Alternative
|
|
364
364
|
|
|
365
|
+
# Request Body Capture
|
|
366
|
+
SECURENOW_CAPTURE_BODY=1 # Capture JSON/form/GraphQL request bodies
|
|
367
|
+
SECURENOW_MAX_BODY_SIZE=10240 # Max body size in bytes (default: 10KB)
|
|
368
|
+
SECURENOW_SENSITIVE_FIELDS="field1,field2" # Additional fields to redact
|
|
369
|
+
|
|
370
|
+
# Multipart Body Capture (v5.8.0+)
|
|
371
|
+
SECURENOW_CAPTURE_MULTIPART=1 # Capture multipart field values & file metadata (streaming)
|
|
372
|
+
|
|
365
373
|
# Debugging
|
|
366
374
|
OTEL_LOG_LEVEL=debug # Enable debug output
|
|
367
375
|
```
|
package/NPM_README.md
CHANGED
|
@@ -903,6 +903,7 @@ export default defineNuxtConfig({
|
|
|
903
903
|
| `SECURENOW_CAPTURE_BODY` | Enable request body capture in traces. Set to `1` to enable. | `0` |
|
|
904
904
|
| `SECURENOW_MAX_BODY_SIZE` | Maximum body size to capture in bytes. Bodies larger than this are truncated. | `10240` (10KB) |
|
|
905
905
|
| `SECURENOW_SENSITIVE_FIELDS` | Comma-separated list of additional field names to redact. | - |
|
|
906
|
+
| `SECURENOW_CAPTURE_MULTIPART` | Enable multipart/form-data capture. Streams through the request to extract text field values and file metadata (name, filename, content-type, size) without buffering file content. Set to `1` to enable. | `0` |
|
|
906
907
|
|
|
907
908
|
**Default sensitive fields (auto-redacted):** `password`, `passwd`, `pwd`, `secret`, `token`, `api_key`, `apikey`, `access_token`, `auth`, `credentials`, `mysql_pwd`, `stripeToken`, `card`, `cardnumber`, `ccv`, `cvc`, `cvv`, `ssn`, `pin`
|
|
908
909
|
|
|
@@ -1018,8 +1019,28 @@ export SECURENOW_MAX_BODY_SIZE=10240 # 10KB (optional)
|
|
|
1018
1019
|
- `application/json`
|
|
1019
1020
|
- `application/x-www-form-urlencoded`
|
|
1020
1021
|
- `application/graphql`
|
|
1022
|
+
- `multipart/form-data` (requires `SECURENOW_CAPTURE_MULTIPART=1`)
|
|
1021
1023
|
|
|
1022
|
-
|
|
1024
|
+
### Multipart Body Capture (v5.8.0+)
|
|
1025
|
+
|
|
1026
|
+
Enable with `SECURENOW_CAPTURE_MULTIPART=1` to capture multipart/form-data requests. Uses a streaming parser that never buffers file content — memory stays at ~few KB regardless of upload size.
|
|
1027
|
+
|
|
1028
|
+
**What gets captured:**
|
|
1029
|
+
- **Text fields** — field name and value (up to 1000 chars), with sensitive fields auto-redacted
|
|
1030
|
+
- **File fields** — metadata only: field name, filename, content-type, and size in bytes
|
|
1031
|
+
|
|
1032
|
+
**Example trace attribute:**
|
|
1033
|
+
```json
|
|
1034
|
+
{
|
|
1035
|
+
"fields": { "description": "My upload", "token": "[REDACTED]" },
|
|
1036
|
+
"files": [
|
|
1037
|
+
{ "field": "avatar", "filename": "photo.jpg", "contentType": "image/jpeg", "size": 524288 },
|
|
1038
|
+
{ "field": "resume", "filename": "cv.pdf", "contentType": "application/pdf", "size": 1048576 }
|
|
1039
|
+
]
|
|
1040
|
+
}
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
File binary content is never stored in traces.
|
|
1023
1044
|
|
|
1024
1045
|
### Sensitive Data Redaction
|
|
1025
1046
|
|
package/README.md
CHANGED
|
@@ -168,6 +168,9 @@ OTEL_EXPORTER_OTLP_HEADERS="x-api-key=..." # Authentication headers
|
|
|
168
168
|
SECURENOW_CAPTURE_BODY=1 # Capture request bodies in traces
|
|
169
169
|
SECURENOW_MAX_BODY_SIZE=10240 # Max body size in bytes
|
|
170
170
|
SECURENOW_SENSITIVE_FIELDS="field1,field2" # Additional fields to redact
|
|
171
|
+
|
|
172
|
+
# Optional: Multipart body capture (file upload metadata)
|
|
173
|
+
SECURENOW_CAPTURE_MULTIPART=1 # Capture multipart field names, values & file metadata
|
|
171
174
|
```
|
|
172
175
|
|
|
173
176
|
### Legacy Environment Variables (still supported)
|
package/cli/auth.js
CHANGED
|
@@ -10,7 +10,7 @@ function openBrowser(url) {
|
|
|
10
10
|
try {
|
|
11
11
|
const platform = process.platform;
|
|
12
12
|
if (platform === 'darwin') execFileSync('open', [url], { stdio: 'ignore' });
|
|
13
|
-
else if (platform === 'win32') execFileSync('
|
|
13
|
+
else if (platform === 'win32') execFileSync('rundll32', ['url.dll,FileProtocolHandler', url], { stdio: 'ignore' });
|
|
14
14
|
else execFileSync('xdg-open', [url], { stdio: 'ignore' });
|
|
15
15
|
return true;
|
|
16
16
|
} catch {
|
package/cli/config.js
CHANGED
|
@@ -32,6 +32,12 @@ function loadJSON(filepath) {
|
|
|
32
32
|
function saveJSON(filepath, data) {
|
|
33
33
|
ensureDir();
|
|
34
34
|
fs.writeFileSync(filepath, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
35
|
+
if (process.platform === 'win32') {
|
|
36
|
+
try {
|
|
37
|
+
const { execFileSync } = require('child_process');
|
|
38
|
+
execFileSync('icacls', [filepath, '/inheritance:r', '/grant:r', `${process.env.USERNAME}:F`], { stdio: 'ignore' });
|
|
39
|
+
} catch (_) {}
|
|
40
|
+
}
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
function loadConfig() {
|
|
@@ -16,6 +16,7 @@ Complete reference for all environment variables supported by SecureNow.
|
|
|
16
16
|
| **SECURENOW_CAPTURE_BODY** | Optional | `0` | Enable request body capture |
|
|
17
17
|
| **SECURENOW_MAX_BODY_SIZE** | Optional | `10240` | Max body size in bytes |
|
|
18
18
|
| **SECURENOW_SENSITIVE_FIELDS** | Optional | - | Comma-separated list of fields to redact |
|
|
19
|
+
| **SECURENOW_CAPTURE_MULTIPART** | Optional | `0` | Enable multipart/form-data streaming capture |
|
|
19
20
|
| **SECURENOW_DISABLE_INSTRUMENTATIONS** | Optional | - | Comma-separated list of packages to disable |
|
|
20
21
|
| **SECURENOW_TEST_SPAN** | Optional | `0` | Emit test span on startup |
|
|
21
22
|
| **OTEL_SERVICE_NAME** | Optional | - | Alternative to SECURENOW_APPID |
|
|
@@ -279,8 +280,8 @@ export SECURENOW_CAPTURE_BODY=1
|
|
|
279
280
|
- `application/x-www-form-urlencoded`
|
|
280
281
|
- `application/graphql`
|
|
281
282
|
|
|
282
|
-
**Not captured:**
|
|
283
|
-
- `multipart/form-data` (
|
|
283
|
+
**Not captured (unless separately enabled):**
|
|
284
|
+
- `multipart/form-data` — requires `SECURENOW_CAPTURE_MULTIPART=1` (see below)
|
|
284
285
|
- Bodies larger than `SECURENOW_MAX_BODY_SIZE`
|
|
285
286
|
|
|
286
287
|
**Security:**
|
|
@@ -346,6 +347,50 @@ export SECURENOW_SENSITIVE_FIELDS="custom_secret,private_data,internal_id"
|
|
|
346
347
|
|
|
347
348
|
---
|
|
348
349
|
|
|
350
|
+
### SECURENOW_CAPTURE_MULTIPART
|
|
351
|
+
|
|
352
|
+
**Description:** Enable capture of `multipart/form-data` request bodies (file upload metadata and text fields). Uses a streaming parser that processes boundary markers on the fly — file binary content is never buffered or stored.
|
|
353
|
+
|
|
354
|
+
**Format:** `1` (enabled) or `0` (disabled)
|
|
355
|
+
|
|
356
|
+
**Default:** `0` (disabled)
|
|
357
|
+
|
|
358
|
+
**Example:**
|
|
359
|
+
```bash
|
|
360
|
+
export SECURENOW_CAPTURE_MULTIPART=1
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**What gets captured:**
|
|
364
|
+
- **Text fields** — field name and value (up to 1000 characters), with sensitive fields auto-redacted
|
|
365
|
+
- **File fields** — metadata only: field name, filename, content-type, and size in bytes (no binary content)
|
|
366
|
+
|
|
367
|
+
**Example span attribute (`http.request.body`):**
|
|
368
|
+
```json
|
|
369
|
+
{
|
|
370
|
+
"fields": { "description": "My upload", "token": "[REDACTED]" },
|
|
371
|
+
"files": [
|
|
372
|
+
{ "field": "avatar", "filename": "photo.jpg", "contentType": "image/jpeg", "size": 524288 },
|
|
373
|
+
{ "field": "document", "filename": "report.pdf", "contentType": "application/pdf", "size": 1048576 }
|
|
374
|
+
]
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Additional span attributes set:**
|
|
379
|
+
- `http.request.body.type` = `"multipart"`
|
|
380
|
+
- `http.request.body.size` — total raw request body size in bytes
|
|
381
|
+
- `http.request.body.fields_count` — number of text fields
|
|
382
|
+
- `http.request.body.files_count` — number of file fields
|
|
383
|
+
|
|
384
|
+
**Memory:** Bounded at ~few KB regardless of upload size (streaming parser discards file content as it passes through).
|
|
385
|
+
|
|
386
|
+
**Parts limit:** 100 parts maximum per request (safety guard).
|
|
387
|
+
|
|
388
|
+
**Requires:** `SECURENOW_CAPTURE_BODY=1` must also be set (multipart capture is gated behind general body capture).
|
|
389
|
+
|
|
390
|
+
**Since:** v5.8.0
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
349
394
|
## Instrumentation Control
|
|
350
395
|
|
|
351
396
|
### SECURENOW_DISABLE_INSTRUMENTATIONS
|
|
@@ -192,6 +192,7 @@ import express from 'express';
|
|
|
192
192
|
| `SECURENOW_CAPTURE_BODY` | Enable body capture (`1` or `true`) | `0` (disabled) |
|
|
193
193
|
| `SECURENOW_MAX_BODY_SIZE` | Max body size in bytes | `10240` (10KB) |
|
|
194
194
|
| `SECURENOW_SENSITIVE_FIELDS` | Comma-separated additional sensitive fields | (see below) |
|
|
195
|
+
| `SECURENOW_CAPTURE_MULTIPART` | Enable multipart/form-data streaming capture (`1` or `true`) | `0` (disabled) |
|
|
195
196
|
|
|
196
197
|
### Default Sensitive Fields
|
|
197
198
|
|
|
@@ -274,10 +275,10 @@ pm2 logs express-api --lines 100
|
|
|
274
275
|
| `application/json` | ✅ Yes | ✅ Yes | ✅ Yes |
|
|
275
276
|
| `application/graphql` | ✅ Yes | ✅ Yes | ✅ Yes |
|
|
276
277
|
| `application/x-www-form-urlencoded` | ✅ Yes | ✅ Yes | ✅ Yes |
|
|
277
|
-
| `multipart/form-data` |
|
|
278
|
+
| `multipart/form-data` | ✅ Metadata | ✅ Streaming | ✅ Yes |
|
|
278
279
|
| `text/plain` | ❌ No | N/A | N/A |
|
|
279
280
|
|
|
280
|
-
**Note**:
|
|
281
|
+
**Note**: Multipart capture requires `SECURENOW_CAPTURE_MULTIPART=1` (v5.8.0+). Uses a streaming parser — text field values and file metadata (name, filename, content-type, size) are captured; file binary content is never buffered or stored.
|
|
281
282
|
|
|
282
283
|
## 🔍 Example: Complete Express + PM2 Setup
|
|
283
284
|
|
|
@@ -55,16 +55,24 @@ SECURENOW_SENSITIVE_FIELDS=credit_card,email,phone
|
|
|
55
55
|
```
|
|
56
56
|
Parsed into object and sensitive fields redacted.
|
|
57
57
|
|
|
58
|
-
4. **Multipart** (`multipart/form-data`) -
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
4. **Multipart** (`multipart/form-data`) - ✅ Streaming Metadata Capture (v5.8.0+)
|
|
59
|
+
|
|
60
|
+
Requires `SECURENOW_CAPTURE_MULTIPART=1`. Uses a streaming parser — file binary content is never buffered.
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"fields": { "description": "Profile update", "token": "[REDACTED]" },
|
|
65
|
+
"files": [
|
|
66
|
+
{ "field": "avatar", "filename": "photo.jpg", "contentType": "image/jpeg", "size": 524288 }
|
|
67
|
+
]
|
|
68
|
+
}
|
|
61
69
|
```
|
|
62
|
-
|
|
70
|
+
Text field values are captured with sensitive-field redaction. File parts record metadata only (field, filename, content-type, size). Memory stays at ~few KB regardless of upload size.
|
|
63
71
|
|
|
64
72
|
### ❌ What's NOT Captured
|
|
65
73
|
|
|
66
74
|
- GET requests (no body)
|
|
67
|
-
- File
|
|
75
|
+
- File binary content (only metadata when multipart capture is enabled)
|
|
68
76
|
- Bodies larger than max size
|
|
69
77
|
- Binary data
|
|
70
78
|
- Non-POST/PUT/PATCH requests
|
|
@@ -218,6 +226,7 @@ mutation Login {
|
|
|
218
226
|
| `SECURENOW_CAPTURE_BODY` | `0` (disabled) | Enable body capture |
|
|
219
227
|
| `SECURENOW_MAX_BODY_SIZE` | `10240` (10KB) | Maximum body size to capture |
|
|
220
228
|
| `SECURENOW_SENSITIVE_FIELDS` | `` | Comma-separated custom sensitive fields |
|
|
229
|
+
| `SECURENOW_CAPTURE_MULTIPART` | `0` (disabled) | Enable multipart/form-data streaming capture (v5.8.0+) |
|
|
221
230
|
|
|
222
231
|
### Programmatic (Next.js)
|
|
223
232
|
|
|
@@ -529,7 +538,7 @@ SECURENOW_SENSITIVE_FIELDS="" # Don't do this!
|
|
|
529
538
|
|
|
530
539
|
### Q: Does this work with file uploads?
|
|
531
540
|
|
|
532
|
-
**A:**
|
|
541
|
+
**A:** Yes! Since v5.8.0, set `SECURENOW_CAPTURE_MULTIPART=1` to capture multipart/form-data requests. The streaming parser extracts text field values and file metadata (name, filename, content-type, size) without ever buffering file binary content. Memory stays bounded at ~few KB regardless of upload size.
|
|
533
542
|
|
|
534
543
|
### Q: What's the performance impact?
|
|
535
544
|
|
package/free-trial-banner.js
CHANGED
|
@@ -107,7 +107,8 @@ function patchHttpForBanner() {
|
|
|
107
107
|
if (res._snIsHtml === undefined) {
|
|
108
108
|
var ct = res.getHeader('content-type');
|
|
109
109
|
var ce = res.getHeader('content-encoding');
|
|
110
|
-
|
|
110
|
+
var csp = res.getHeader('content-security-policy');
|
|
111
|
+
res._snIsHtml = !!(ct && String(ct).includes('text/html') && !ce && !csp);
|
|
111
112
|
}
|
|
112
113
|
if (!res._snIsHtml) {
|
|
113
114
|
res._snBannerDone = true;
|
|
@@ -136,6 +137,11 @@ function patchHttpForBanner() {
|
|
|
136
137
|
if (modified !== chunk) {
|
|
137
138
|
var enc = typeof encoding === 'function' ? 'utf8' : encoding;
|
|
138
139
|
var callback = typeof encoding === 'function' ? encoding : cb;
|
|
140
|
+
try {
|
|
141
|
+
if (this.getHeader('content-length')) {
|
|
142
|
+
this.setHeader('content-length', Buffer.byteLength(modified));
|
|
143
|
+
}
|
|
144
|
+
} catch (_) { /* headers already sent */ }
|
|
139
145
|
return _origWrite.call(this, modified, enc, callback);
|
|
140
146
|
}
|
|
141
147
|
} catch (_) { /* never break the app */ }
|
package/nextjs-auto-capture.js
CHANGED
|
@@ -33,7 +33,7 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
33
33
|
|
|
34
34
|
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
35
35
|
|
|
36
|
-
for (const key
|
|
36
|
+
for (const key of Object.keys(redacted)) {
|
|
37
37
|
const lowerKey = key.toLowerCase();
|
|
38
38
|
|
|
39
39
|
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
package/nextjs-middleware.js
CHANGED
|
@@ -27,12 +27,16 @@ const DEFAULT_SENSITIVE_FIELDS = [
|
|
|
27
27
|
/**
|
|
28
28
|
* Redact sensitive fields from an object
|
|
29
29
|
*/
|
|
30
|
+
function escapeRegex(str) {
|
|
31
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
31
35
|
if (!obj || typeof obj !== 'object') return obj;
|
|
32
36
|
|
|
33
37
|
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
34
38
|
|
|
35
|
-
for (const key
|
|
39
|
+
for (const key of Object.keys(redacted)) {
|
|
36
40
|
const lowerKey = key.toLowerCase();
|
|
37
41
|
|
|
38
42
|
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
@@ -54,9 +58,10 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
54
58
|
let redacted = query;
|
|
55
59
|
|
|
56
60
|
sensitiveFields.forEach(field => {
|
|
61
|
+
const escaped = escapeRegex(field);
|
|
57
62
|
const patterns = [
|
|
58
|
-
new RegExp(`(${
|
|
59
|
-
new RegExp(`(${
|
|
63
|
+
new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
64
|
+
new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
60
65
|
];
|
|
61
66
|
|
|
62
67
|
patterns.forEach(pattern => {
|
package/nextjs-wrapper.js
CHANGED
|
@@ -32,7 +32,7 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
32
32
|
|
|
33
33
|
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
34
34
|
|
|
35
|
-
for (const key
|
|
35
|
+
for (const key of Object.keys(redacted)) {
|
|
36
36
|
const lowerKey = key.toLowerCase();
|
|
37
37
|
|
|
38
38
|
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
package/package.json
CHANGED
package/tracing.js
CHANGED
|
@@ -53,6 +53,10 @@ const DEFAULT_SENSITIVE_FIELDS = [
|
|
|
53
53
|
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
54
54
|
];
|
|
55
55
|
|
|
56
|
+
function escapeRegex(str) {
|
|
57
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
58
|
+
}
|
|
59
|
+
|
|
56
60
|
/**
|
|
57
61
|
* Redact sensitive fields from an object
|
|
58
62
|
*/
|
|
@@ -61,7 +65,7 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
61
65
|
|
|
62
66
|
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
63
67
|
|
|
64
|
-
for (const key
|
|
68
|
+
for (const key of Object.keys(redacted)) {
|
|
65
69
|
const lowerKey = key.toLowerCase();
|
|
66
70
|
|
|
67
71
|
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
@@ -84,10 +88,10 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
84
88
|
|
|
85
89
|
// Redact sensitive fields in GraphQL arguments and variables
|
|
86
90
|
sensitiveFields.forEach(field => {
|
|
87
|
-
|
|
91
|
+
const escaped = escapeRegex(field);
|
|
88
92
|
const patterns = [
|
|
89
|
-
new RegExp(`(${
|
|
90
|
-
new RegExp(`(${
|
|
93
|
+
new RegExp(`(${escaped}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
94
|
+
new RegExp(`(${escaped}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
91
95
|
];
|
|
92
96
|
|
|
93
97
|
patterns.forEach(pattern => {
|
|
@@ -118,7 +122,7 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
|
|
|
118
122
|
const boundary = extractBoundary(contentType);
|
|
119
123
|
if (!boundary) { onComplete({ error: 'BOUNDARY_NOT_FOUND' }); return; }
|
|
120
124
|
|
|
121
|
-
const result = { fields:
|
|
125
|
+
const result = { fields: Object.create(null), files: [] };
|
|
122
126
|
let totalSize = 0;
|
|
123
127
|
let buf = Buffer.alloc(0);
|
|
124
128
|
|
|
@@ -139,7 +143,7 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
|
|
|
139
143
|
let textVal = '';
|
|
140
144
|
|
|
141
145
|
function flushPart() {
|
|
142
|
-
if (!fldName) return;
|
|
146
|
+
if (!fldName || fldName === '__proto__' || fldName === 'constructor' || fldName === 'prototype') return;
|
|
143
147
|
if (isFile) {
|
|
144
148
|
result.files.push({ field: fldName, filename: fName, contentType: pCT || 'unknown', size: bodyBytes });
|
|
145
149
|
} else {
|
|
@@ -316,7 +320,7 @@ for (const n of (env('SECURENOW_DISABLE_INSTRUMENTATIONS') || '').split(',').map
|
|
|
316
320
|
|
|
317
321
|
// -------- Body Capture Configuration --------
|
|
318
322
|
const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' || String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true';
|
|
319
|
-
const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') ||
|
|
323
|
+
const maxBodySize = Math.max(1024, parseInt(env('SECURENOW_MAX_BODY_SIZE'), 10) || 10240);
|
|
320
324
|
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
321
325
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
322
326
|
|
|
@@ -328,7 +332,7 @@ const captureMultipart = String(env('SECURENOW_CAPTURE_MULTIPART')) === '1' || S
|
|
|
328
332
|
// This prevents end-users from spoofing their IP via custom headers.
|
|
329
333
|
const os = require('os');
|
|
330
334
|
const LOOPBACK_RE = /^(127\.|::1$|::ffff:127\.)/;
|
|
331
|
-
const PRIVATE_IP_RE = /^(127\.|::1
|
|
335
|
+
const PRIVATE_IP_RE = /^(127\.|::1$|::ffff:127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|f[cd][0-9a-f]{2}:)/;
|
|
332
336
|
const trustedProxyCsv = (env('SECURENOW_TRUSTED_PROXIES') || '').trim();
|
|
333
337
|
const trustedProxySet = trustedProxyCsv ? new Set(trustedProxyCsv.split(',').map(s => s.trim()).filter(Boolean)) : null;
|
|
334
338
|
|
|
@@ -443,9 +447,9 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
443
447
|
});
|
|
444
448
|
}
|
|
445
449
|
} catch (e) {
|
|
446
|
-
|
|
447
|
-
span.setAttribute('http.request.body', body.substring(0, 1000));
|
|
450
|
+
span.setAttribute('http.request.body', '[UNPARSEABLE - REDACTED FOR SAFETY]');
|
|
448
451
|
span.setAttribute('http.request.body.parse_error', true);
|
|
452
|
+
span.setAttribute('http.request.body.size', size);
|
|
449
453
|
}
|
|
450
454
|
} else if (size > maxBodySize) {
|
|
451
455
|
span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
|
package/web-vite.mjs
CHANGED
|
@@ -55,10 +55,20 @@ const baseName = rawBase || null;
|
|
|
55
55
|
const noUuid = String(env('SECURENOW_NO_UUID')) === '1' || String(env('SECURENOW_NO_UUID')).toLowerCase() === 'true';
|
|
56
56
|
const strict = String(env('SECURENOW_STRICT')) === '1' || String(env('SECURENOW_STRICT')).toLowerCase() === 'true';
|
|
57
57
|
|
|
58
|
-
// Simple UUID v4 (no crypto dependency needed)
|
|
59
58
|
function uuidv4(): string {
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
60
|
+
return crypto.randomUUID();
|
|
61
|
+
}
|
|
62
|
+
const bytes = new Uint8Array(16);
|
|
63
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
64
|
+
crypto.getRandomValues(bytes);
|
|
65
|
+
} else {
|
|
66
|
+
for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
67
|
+
}
|
|
68
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
69
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
70
|
+
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
71
|
+
return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
let serviceName: string;
|
|
@@ -124,11 +134,11 @@ export function startSecurenowWeb() {
|
|
|
124
134
|
new DocumentLoadInstrumentation(),
|
|
125
135
|
new UserInteractionInstrumentation(),
|
|
126
136
|
new FetchInstrumentation({
|
|
127
|
-
propagateTraceHeaderCorsUrls: [
|
|
137
|
+
propagateTraceHeaderCorsUrls: [new RegExp(`^${location.origin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)],
|
|
128
138
|
ignoreUrls: [/\/vite\/hmr/, /^chrome-extension:\/\//, /sockjs/],
|
|
129
139
|
}),
|
|
130
140
|
new XMLHttpRequestInstrumentation({
|
|
131
|
-
propagateTraceHeaderCorsUrls: [
|
|
141
|
+
propagateTraceHeaderCorsUrls: [new RegExp(`^${location.origin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)],
|
|
132
142
|
}),
|
|
133
143
|
],
|
|
134
144
|
});
|