payload-guard-filter 1.0.1 → 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/dist/core/compiler.d.ts +4 -0
- package/dist/core/compiler.js +12 -3
- package/dist/core/types.d.ts +13 -1
- package/dist/middleware/express.js +41 -10
- package/package.json +8 -6
package/dist/core/compiler.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ interface CompileOptions {
|
|
|
5
5
|
strict?: boolean;
|
|
6
6
|
redact?: (key: string, value: unknown) => unknown;
|
|
7
7
|
logger?: (msg: string, level?: 'warn' | 'error' | 'info') => void;
|
|
8
|
+
/** Maximum array length to process (memory safety) */
|
|
9
|
+
maxArrayLength?: number;
|
|
10
|
+
/** Parent path for nested field warnings (internal) */
|
|
11
|
+
_parentPath?: string;
|
|
8
12
|
}
|
|
9
13
|
/**
|
|
10
14
|
* Compile a shape descriptor into an optimized filter function
|
package/dist/core/compiler.js
CHANGED
|
@@ -57,11 +57,20 @@ function compile(shape, opts = {}) {
|
|
|
57
57
|
}
|
|
58
58
|
// Array shape
|
|
59
59
|
if (isArrayShape(shape)) {
|
|
60
|
-
const itemFn = compile(shape.item, opts);
|
|
60
|
+
const itemFn = compile(shape.item, { ...opts, _parentPath: (opts._parentPath || '') + '[]' });
|
|
61
61
|
return (value) => {
|
|
62
62
|
if (!Array.isArray(value))
|
|
63
63
|
return [];
|
|
64
|
-
|
|
64
|
+
// Memory safety: slice array if maxArrayLength is set
|
|
65
|
+
let arr = value;
|
|
66
|
+
if (opts.maxArrayLength && arr.length > opts.maxArrayLength) {
|
|
67
|
+
if (opts.dev) {
|
|
68
|
+
const path = opts._parentPath || 'array';
|
|
69
|
+
(opts.logger ?? console.warn)(`[payload-guard] ${path}: array truncated from ${arr.length} to ${opts.maxArrayLength} items (maxArrayLength)`, 'warn');
|
|
70
|
+
}
|
|
71
|
+
arr = arr.slice(0, opts.maxArrayLength);
|
|
72
|
+
}
|
|
73
|
+
return arr.map(item => {
|
|
65
74
|
try {
|
|
66
75
|
return itemFn(item);
|
|
67
76
|
}
|
|
@@ -92,7 +101,7 @@ function compile(shape, opts = {}) {
|
|
|
92
101
|
}
|
|
93
102
|
continue;
|
|
94
103
|
}
|
|
95
|
-
fieldFilters[key] = compile(shapeObj[key], opts);
|
|
104
|
+
fieldFilters[key] = compile(shapeObj[key], { ...opts, _parentPath: (opts._parentPath ? opts._parentPath + '.' : '') + key });
|
|
96
105
|
}
|
|
97
106
|
return (value) => {
|
|
98
107
|
if (value == null || typeof value !== 'object')
|
package/dist/core/types.d.ts
CHANGED
|
@@ -31,14 +31,26 @@ export interface GuardConfig {
|
|
|
31
31
|
strict?: boolean;
|
|
32
32
|
/** Redaction hook: return replacement value for a key */
|
|
33
33
|
redact?: (key: string, value: unknown) => unknown;
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Logger integration hook for custom logging (pino, winston, etc.)
|
|
36
|
+
* @example
|
|
37
|
+
* logger: (msg, level) => pino[level || 'info'](msg)
|
|
38
|
+
*/
|
|
35
39
|
logger?: (msg: string, level?: 'warn' | 'error' | 'info') => void;
|
|
36
40
|
/** Maximum payload size (bytes) for middleware processing; if exceeded, processing is skipped */
|
|
37
41
|
maxPayloadSize?: number;
|
|
42
|
+
/** Skip processing if payload exceeds maxPayloadSize (default: true) */
|
|
43
|
+
skipLargePayload?: boolean;
|
|
44
|
+
/** Maximum array length to process (memory safety) */
|
|
45
|
+
maxArrayLength?: number;
|
|
38
46
|
/** Routes to ignore (prefix match) */
|
|
39
47
|
ignoreRoutes?: string[];
|
|
40
48
|
/** Enable payload metrics (measure before/after sizes) */
|
|
41
49
|
payloadMetrics?: boolean;
|
|
50
|
+
/** Sanitize request headers (remove sensitive headers) */
|
|
51
|
+
sanitizeHeaders?: boolean;
|
|
52
|
+
/** Sensitive header names to filter (default: authorization, cookie, x-api-key) */
|
|
53
|
+
sensitiveHeaders?: string[];
|
|
42
54
|
}
|
|
43
55
|
export interface GuardMiddlewareOptions extends GuardConfig {
|
|
44
56
|
/** Sanitize request body */
|
|
@@ -19,10 +19,12 @@ const security_1 = require("../core/security");
|
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
function guardMiddleware(opts = {}) {
|
|
22
|
-
const { sanitizeBody = false, requestShape, sensitiveFields = [], devMode = false, filterResponse = false, maxPayloadSize, ignoreRoutes = [], payloadMetrics = false, } = opts;
|
|
22
|
+
const { sanitizeBody = false, requestShape, sensitiveFields = [], devMode = false, filterResponse = false, maxPayloadSize, skipLargePayload = true, maxArrayLength, ignoreRoutes = [], payloadMetrics = false, sanitizeHeaders = false, sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token'], } = opts;
|
|
23
|
+
// Default sensitive headers
|
|
24
|
+
const DEFAULT_SENSITIVE_HEADERS = ['authorization', 'cookie', 'x-api-key', 'x-auth-token', 'x-session-id'];
|
|
23
25
|
// Pre-compile request shape if provided
|
|
24
26
|
const compiledRequestShape = requestShape
|
|
25
|
-
? (0, compiler_1.compile)(requestShape, { sensitive: sensitiveFields, dev: devMode })
|
|
27
|
+
? (0, compiler_1.compile)(requestShape, { sensitive: sensitiveFields, dev: devMode, maxArrayLength, logger: opts.logger })
|
|
26
28
|
: null;
|
|
27
29
|
// Response filter cache for shapes to avoid recompiling per request
|
|
28
30
|
const responseFilterCache = new WeakMap();
|
|
@@ -32,6 +34,20 @@ function guardMiddleware(opts = {}) {
|
|
|
32
34
|
if (ignoreRoutes && ignoreRoutes.some(p => req.path.startsWith(p))) {
|
|
33
35
|
return next();
|
|
34
36
|
}
|
|
37
|
+
const routeName = `[${req.method}] ${req.path}`;
|
|
38
|
+
// Sanitize headers if enabled
|
|
39
|
+
if (sanitizeHeaders && req.headers) {
|
|
40
|
+
const allSensitiveHeaders = [...DEFAULT_SENSITIVE_HEADERS, ...(sensitiveHeaders || [])];
|
|
41
|
+
for (const header of allSensitiveHeaders) {
|
|
42
|
+
const headerLower = header.toLowerCase();
|
|
43
|
+
if (req.headers[headerLower]) {
|
|
44
|
+
if (devMode) {
|
|
45
|
+
(opts.logger ?? console.warn)(`[payload-guard] ${routeName}: Sensitive header "${header}" removed`, 'warn');
|
|
46
|
+
}
|
|
47
|
+
delete req.headers[headerLower];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
35
51
|
// Sanitize request body
|
|
36
52
|
if (sanitizeBody && req.body && typeof req.body === 'object') {
|
|
37
53
|
// max payload size check
|
|
@@ -40,9 +56,17 @@ function guardMiddleware(opts = {}) {
|
|
|
40
56
|
const size = Buffer.byteLength(JSON.stringify(req.body || ''), 'utf8');
|
|
41
57
|
if (size > maxPayloadSize) {
|
|
42
58
|
if (devMode) {
|
|
43
|
-
opts.logger
|
|
59
|
+
(opts.logger ?? console.warn)(`[payload-guard] ${routeName}: payload exceeds maxPayloadSize (${size} > ${maxPayloadSize} bytes)`, 'warn');
|
|
60
|
+
}
|
|
61
|
+
// Skip processing if skipLargePayload is true (default), else still process
|
|
62
|
+
if (!skipLargePayload) {
|
|
63
|
+
if (compiledRequestShape) {
|
|
64
|
+
req.body = compiledRequestShape(req.body);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
req.body = (0, security_1.removeSensitive)(req.body, sensitiveFields);
|
|
68
|
+
}
|
|
44
69
|
}
|
|
45
|
-
// do not block; skip processing
|
|
46
70
|
}
|
|
47
71
|
else {
|
|
48
72
|
if (compiledRequestShape) {
|
|
@@ -56,7 +80,7 @@ function guardMiddleware(opts = {}) {
|
|
|
56
80
|
catch (e) {
|
|
57
81
|
// if serialization fails, skip and continue
|
|
58
82
|
if (devMode)
|
|
59
|
-
opts.logger
|
|
83
|
+
(opts.logger ?? console.warn)('[payload-guard] error checking payload size: ' + String(e), 'warn');
|
|
60
84
|
}
|
|
61
85
|
}
|
|
62
86
|
else {
|
|
@@ -68,13 +92,20 @@ function guardMiddleware(opts = {}) {
|
|
|
68
92
|
}
|
|
69
93
|
}
|
|
70
94
|
}
|
|
71
|
-
// Dev mode: warn about sensitive fields in request
|
|
95
|
+
// Dev mode: warn about sensitive fields in request with full path
|
|
72
96
|
if (devMode && req.body && typeof req.body === 'object') {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
const warnSensitive = (obj, path) => {
|
|
98
|
+
for (const key of Object.keys(obj)) {
|
|
99
|
+
const fullPath = path ? `${path}.${key}` : key;
|
|
100
|
+
if ((0, security_1.isSensitive)(key, sensitiveFields)) {
|
|
101
|
+
(opts.logger ?? console.warn)(`[payload-guard] ${routeName}: Unexpected field: ${fullPath}`, 'warn');
|
|
102
|
+
}
|
|
103
|
+
if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
|
104
|
+
warnSensitive(obj[key], fullPath);
|
|
105
|
+
}
|
|
76
106
|
}
|
|
77
|
-
}
|
|
107
|
+
};
|
|
108
|
+
warnSensitive(req.body, '');
|
|
78
109
|
}
|
|
79
110
|
// Attach guardJson helper to response
|
|
80
111
|
res.guardJson = function (shape, data) {
|
package/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payload-guard-filter",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Lightweight, zero-dependency shape-based payload filtering and sanitization for Node.js and browser",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
|
-
"module": "dist/index.
|
|
6
|
+
"module": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"import": "./dist/index.
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
11
|
"require": "./dist/index.js",
|
|
12
12
|
"types": "./dist/index.d.ts"
|
|
13
13
|
},
|
|
14
14
|
"./express": {
|
|
15
|
-
"import": "./dist/express.
|
|
15
|
+
"import": "./dist/express.js",
|
|
16
16
|
"require": "./dist/express.js",
|
|
17
17
|
"types": "./dist/express.d.ts"
|
|
18
18
|
},
|
|
19
19
|
"./client": {
|
|
20
|
-
"import": "./dist/client.
|
|
20
|
+
"import": "./dist/client.js",
|
|
21
21
|
"require": "./dist/client.js",
|
|
22
22
|
"types": "./dist/client.d.ts"
|
|
23
23
|
}
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"test:watch": "vitest",
|
|
42
42
|
"test:coverage": "vitest run --coverage",
|
|
43
43
|
"lint": "eslint src --ext .ts",
|
|
44
|
-
"format": "prettier --write \"src/**/*.ts\""
|
|
44
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
45
|
+
"benchmark": "npx ts-node benchmarks/run.ts"
|
|
45
46
|
},
|
|
46
47
|
"keywords": [
|
|
47
48
|
"payload",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
],
|
|
60
61
|
"author": "",
|
|
61
62
|
"license": "MIT",
|
|
63
|
+
"sideEffects": false,
|
|
62
64
|
"engines": {
|
|
63
65
|
"node": ">=18.0.0"
|
|
64
66
|
},
|