hi-secure 1.0.15 → 1.0.16
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/adapters/ArgonAdapter.d.ts +1 -1
- package/dist/adapters/ArgonAdapter.d.ts.map +1 -1
- package/dist/adapters/ArgonAdapter.js +43 -5
- package/dist/adapters/ArgonAdapter.js.map +1 -1
- package/dist/adapters/BcryptAdapter.d.ts.map +1 -1
- package/dist/adapters/BcryptAdapter.js +43 -3
- package/dist/adapters/BcryptAdapter.js.map +1 -1
- package/dist/adapters/ExpressRLAdapter.d.ts.map +1 -1
- package/dist/adapters/ExpressRLAdapter.js +48 -6
- package/dist/adapters/ExpressRLAdapter.js.map +1 -1
- package/dist/adapters/ExpressValidatorAdapter.d.ts.map +1 -1
- package/dist/adapters/ExpressValidatorAdapter.js +50 -10
- package/dist/adapters/ExpressValidatorAdapter.js.map +1 -1
- package/dist/adapters/GoogleAdapter.d.ts.map +1 -1
- package/dist/adapters/GoogleAdapter.js +82 -16
- package/dist/adapters/GoogleAdapter.js.map +1 -1
- package/dist/adapters/JWTAdapter.d.ts.map +1 -1
- package/dist/adapters/JWTAdapter.js +104 -15
- package/dist/adapters/JWTAdapter.js.map +1 -1
- package/dist/adapters/RLFlexibleAdapter.d.ts.map +1 -1
- package/dist/adapters/RLFlexibleAdapter.js +87 -12
- package/dist/adapters/RLFlexibleAdapter.js.map +1 -1
- package/dist/adapters/SanitizeHtmlAdapter.d.ts.map +1 -1
- package/dist/adapters/SanitizeHtmlAdapter.js +81 -13
- package/dist/adapters/SanitizeHtmlAdapter.js.map +1 -1
- package/dist/adapters/XSSAdapter.d.ts +1 -1
- package/dist/adapters/XSSAdapter.d.ts.map +1 -1
- package/dist/adapters/XSSAdapter.js +137 -20
- package/dist/adapters/XSSAdapter.js.map +1 -1
- package/dist/adapters/ZodAdapter.d.ts +1 -1
- package/dist/adapters/ZodAdapter.d.ts.map +1 -1
- package/dist/adapters/ZodAdapter.js +13 -8
- package/dist/adapters/ZodAdapter.js.map +1 -1
- package/dist/core/HiSecure.d.ts +3 -4
- package/dist/core/HiSecure.d.ts.map +1 -1
- package/dist/core/HiSecure.js +108 -121
- package/dist/core/HiSecure.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/logging/index.d.ts.map +1 -1
- package/dist/logging/index.js +2 -0
- package/dist/logging/index.js.map +1 -1
- package/dist/logging/morganSetup.d.ts.map +1 -1
- package/dist/logging/morganSetup.js +22 -1
- package/dist/logging/morganSetup.js.map +1 -1
- package/dist/logging/winstonSetup.d.ts.map +1 -1
- package/dist/logging/winstonSetup.js +61 -3
- package/dist/logging/winstonSetup.js.map +1 -1
- package/dist/managers/AuthManager.d.ts +2 -2
- package/dist/managers/AuthManager.d.ts.map +1 -1
- package/dist/managers/AuthManager.js +167 -31
- package/dist/managers/AuthManager.js.map +1 -1
- package/dist/managers/CorsManager.d.ts.map +1 -1
- package/dist/managers/CorsManager.js +46 -11
- package/dist/managers/CorsManager.js.map +1 -1
- package/dist/managers/HashManager.d.ts +1 -1
- package/dist/managers/HashManager.d.ts.map +1 -1
- package/dist/managers/HashManager.js +127 -17
- package/dist/managers/HashManager.js.map +1 -1
- package/dist/managers/JsonManager.d.ts +1 -1
- package/dist/managers/JsonManager.d.ts.map +1 -1
- package/dist/managers/JsonManager.js +99 -16
- package/dist/managers/JsonManager.js.map +1 -1
- package/dist/managers/RateLimitManager.d.ts +1 -1
- package/dist/managers/RateLimitManager.d.ts.map +1 -1
- package/dist/managers/RateLimitManager.js +46 -22
- package/dist/managers/RateLimitManager.js.map +1 -1
- package/dist/managers/SanitizerManager.d.ts.map +1 -1
- package/dist/managers/SanitizerManager.js +112 -15
- package/dist/managers/SanitizerManager.js.map +1 -1
- package/dist/managers/ValidatorManager.d.ts.map +1 -1
- package/dist/managers/ValidatorManager.js +90 -7
- package/dist/managers/ValidatorManager.js.map +1 -1
- package/package.json +2 -6
- package/readme.md +3 -6
- package/src/adapters/ArgonAdapter.ts +55 -6
- package/src/adapters/BcryptAdapter.ts +56 -8
- package/src/adapters/ExpressRLAdapter.ts +62 -9
- package/src/adapters/ExpressValidatorAdapter.ts +67 -11
- package/src/adapters/GoogleAdapter.ts +106 -21
- package/src/adapters/JWTAdapter.ts +129 -21
- package/src/adapters/RLFlexibleAdapter.ts +113 -16
- package/src/adapters/SanitizeHtmlAdapter.ts +111 -18
- package/src/adapters/XSSAdapter.ts +183 -39
- package/src/adapters/ZodAdapter.ts +56 -10
- package/src/core/HiSecure.ts +496 -162
- package/src/index.ts +4 -0
- package/src/logging/index.ts +6 -0
- package/src/logging/morganSetup.ts +36 -1
- package/src/logging/winstonSetup.ts +97 -8
- package/src/managers/AuthManager.ts +205 -34
- package/src/managers/CorsManager.ts +63 -16
- package/src/managers/HashManager.ts +156 -19
- package/src/managers/JsonManager.ts +119 -15
- package/src/managers/RateLimitManager.ts +174 -29
- package/src/managers/SanitizerManager.ts +150 -25
- package/src/managers/ValidatorManager.ts +115 -15
|
@@ -1,10 +1,92 @@
|
|
|
1
|
+
// import { RateLimiterMemory, RateLimiterRes } from "rate-limiter-flexible";
|
|
2
|
+
// import { logger } from "../logging/index.js";
|
|
3
|
+
// import { AdapterError } from "../core/errors/AdapterError.js";
|
|
4
|
+
|
|
5
|
+
// export interface RLOptions {
|
|
6
|
+
// points?: number;
|
|
7
|
+
// duration?: number;
|
|
8
|
+
// message?: any;
|
|
9
|
+
// blockDuration?: number;
|
|
10
|
+
// }
|
|
11
|
+
|
|
12
|
+
// export class RLFlexibleAdapter {
|
|
13
|
+
// getMiddleware(options: RLOptions = {}) {
|
|
14
|
+
// try {
|
|
15
|
+
// const defaultOptions = {
|
|
16
|
+
// points: 100,
|
|
17
|
+
// duration: 60,
|
|
18
|
+
// message: "Too many requests, slow down.",
|
|
19
|
+
// blockDuration: 0
|
|
20
|
+
// };
|
|
21
|
+
|
|
22
|
+
// const finalOptions = { ...defaultOptions, ...options };
|
|
23
|
+
|
|
24
|
+
// const limiter = new RateLimiterMemory({
|
|
25
|
+
// points: finalOptions.points,
|
|
26
|
+
// duration: finalOptions.duration,
|
|
27
|
+
// blockDuration: finalOptions.blockDuration
|
|
28
|
+
// });
|
|
29
|
+
|
|
30
|
+
// return async (req: any, res: any, next: any) => {
|
|
31
|
+
|
|
32
|
+
// const ip = this.extractIP(req);
|
|
33
|
+
|
|
34
|
+
// try {
|
|
35
|
+
// await limiter.consume(ip);
|
|
36
|
+
// next();
|
|
37
|
+
// } catch (err: any) {
|
|
38
|
+
// const rlErr = err as RateLimiterRes;
|
|
39
|
+
|
|
40
|
+
// logger.warn("RLFlexibleAdapter: rate limit exceeded", {
|
|
41
|
+
// ip,
|
|
42
|
+
// path: req.path,
|
|
43
|
+
// method: req.method,
|
|
44
|
+
// retryAfter: rlErr.msBeforeNext
|
|
45
|
+
// });
|
|
46
|
+
|
|
47
|
+
// res.setHeader('Retry-After', Math.ceil(rlErr.msBeforeNext / 1000));
|
|
48
|
+
|
|
49
|
+
// return res.status(429).json({
|
|
50
|
+
// success: false,
|
|
51
|
+
// error: "RATE_LIMIT_EXCEEDED",
|
|
52
|
+
// retryAfter: Math.ceil(rlErr.msBeforeNext / 1000),
|
|
53
|
+
// message: finalOptions.message
|
|
54
|
+
// });
|
|
55
|
+
// }
|
|
56
|
+
// };
|
|
57
|
+
|
|
58
|
+
// } catch (err: any) {
|
|
59
|
+
// logger.error("RLFlexibleAdapter: failed to initialize limiter", {
|
|
60
|
+
// error: err?.message || err
|
|
61
|
+
// });
|
|
62
|
+
// throw new AdapterError("RateLimiterFlexible creation failed.");
|
|
63
|
+
// }
|
|
64
|
+
// }
|
|
65
|
+
|
|
66
|
+
// private extractIP(req: any): string {
|
|
67
|
+
// return (
|
|
68
|
+
// req.headers['x-real-ip'] ||
|
|
69
|
+
// req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
|
|
70
|
+
// req.ip ||
|
|
71
|
+
// req.connection?.remoteAddress ||
|
|
72
|
+
// req.socket?.remoteAddress ||
|
|
73
|
+
// 'unknown'
|
|
74
|
+
// );
|
|
75
|
+
// }
|
|
76
|
+
// }
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
1
83
|
import { RateLimiterMemory, RateLimiterRes } from "rate-limiter-flexible";
|
|
2
|
-
import { logger } from "../logging
|
|
3
|
-
import { AdapterError } from "../core/errors/AdapterError
|
|
84
|
+
import { logger } from "../logging";
|
|
85
|
+
import { AdapterError } from "../core/errors/AdapterError";
|
|
4
86
|
|
|
5
87
|
export interface RLOptions {
|
|
6
88
|
points?: number;
|
|
7
|
-
duration?: number;
|
|
89
|
+
duration?: number;
|
|
8
90
|
message?: any;
|
|
9
91
|
blockDuration?: number;
|
|
10
92
|
}
|
|
@@ -27,8 +109,16 @@ export class RLFlexibleAdapter {
|
|
|
27
109
|
blockDuration: finalOptions.blockDuration
|
|
28
110
|
});
|
|
29
111
|
|
|
112
|
+
|
|
113
|
+
logger.info("Rate limiter initialized", {
|
|
114
|
+
adapter: "rate-limiter-flexible",
|
|
115
|
+
operation: "init",
|
|
116
|
+
points: finalOptions.points,
|
|
117
|
+
duration: finalOptions.duration,
|
|
118
|
+
blockDuration: finalOptions.blockDuration
|
|
119
|
+
});
|
|
120
|
+
|
|
30
121
|
return async (req: any, res: any, next: any) => {
|
|
31
|
-
|
|
32
122
|
const ip = this.extractIP(req);
|
|
33
123
|
|
|
34
124
|
try {
|
|
@@ -37,15 +127,20 @@ export class RLFlexibleAdapter {
|
|
|
37
127
|
} catch (err: any) {
|
|
38
128
|
const rlErr = err as RateLimiterRes;
|
|
39
129
|
|
|
40
|
-
logger.warn("
|
|
130
|
+
logger.warn("Rate limit exceeded", {
|
|
131
|
+
adapter: "rate-limiter-flexible",
|
|
132
|
+
operation: "consume",
|
|
41
133
|
ip,
|
|
42
|
-
path: req.path,
|
|
43
134
|
method: req.method,
|
|
44
|
-
|
|
135
|
+
path: req.path,
|
|
136
|
+
retryAfterMs: rlErr.msBeforeNext
|
|
45
137
|
});
|
|
46
138
|
|
|
47
|
-
res.setHeader(
|
|
48
|
-
|
|
139
|
+
res.setHeader(
|
|
140
|
+
"Retry-After",
|
|
141
|
+
Math.ceil(rlErr.msBeforeNext / 1000)
|
|
142
|
+
);
|
|
143
|
+
|
|
49
144
|
return res.status(429).json({
|
|
50
145
|
success: false,
|
|
51
146
|
error: "RATE_LIMIT_EXCEEDED",
|
|
@@ -54,23 +149,25 @@ export class RLFlexibleAdapter {
|
|
|
54
149
|
});
|
|
55
150
|
}
|
|
56
151
|
};
|
|
57
|
-
|
|
58
152
|
} catch (err: any) {
|
|
59
|
-
logger.error("
|
|
60
|
-
|
|
153
|
+
logger.error("Rate limiter initialization failed", {
|
|
154
|
+
adapter: "rate-limiter-flexible",
|
|
155
|
+
operation: "init",
|
|
156
|
+
reason: err?.message
|
|
61
157
|
});
|
|
158
|
+
|
|
62
159
|
throw new AdapterError("RateLimiterFlexible creation failed.");
|
|
63
160
|
}
|
|
64
161
|
}
|
|
65
162
|
|
|
66
163
|
private extractIP(req: any): string {
|
|
67
164
|
return (
|
|
68
|
-
req.headers[
|
|
69
|
-
req.headers[
|
|
165
|
+
req.headers["x-real-ip"] ||
|
|
166
|
+
req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
|
|
70
167
|
req.ip ||
|
|
71
168
|
req.connection?.remoteAddress ||
|
|
72
169
|
req.socket?.remoteAddress ||
|
|
73
|
-
|
|
170
|
+
"unknown"
|
|
74
171
|
);
|
|
75
172
|
}
|
|
76
|
-
}
|
|
173
|
+
}
|
|
@@ -1,6 +1,89 @@
|
|
|
1
|
+
// import sanitizeHtml from "sanitize-html";
|
|
2
|
+
// import { AdapterError } from "../core/errors/AdapterError.js";
|
|
3
|
+
// import { logger } from "../logging/index.js";
|
|
4
|
+
|
|
5
|
+
// export class SanitizeHtmlAdapter {
|
|
6
|
+
// private globalOptions: sanitizeHtml.IOptions;
|
|
7
|
+
|
|
8
|
+
// constructor(options: sanitizeHtml.IOptions = {}) {
|
|
9
|
+
// this.globalOptions = options;
|
|
10
|
+
// }
|
|
11
|
+
|
|
12
|
+
// sanitize(input: string, dynamicOptions?: any): string {
|
|
13
|
+
// try {
|
|
14
|
+
// const opts = { ...this.globalOptions, ...(dynamicOptions || {}) };
|
|
15
|
+
|
|
16
|
+
// const clean = sanitizeHtml(input, opts);
|
|
17
|
+
// return typeof clean === "string" ? clean : String(clean);
|
|
18
|
+
|
|
19
|
+
// } catch (err: any) {
|
|
20
|
+
// logger.error("sanitize-html failed", {
|
|
21
|
+
// error: err?.message || err,
|
|
22
|
+
// preview: typeof input === "string" ? input.slice(0, 100) : undefined
|
|
23
|
+
// });
|
|
24
|
+
|
|
25
|
+
// throw new AdapterError("sanitize-html adapter failed.");
|
|
26
|
+
// }
|
|
27
|
+
// }
|
|
28
|
+
|
|
29
|
+
// // Deep Sanitization - Recursively
|
|
30
|
+
// private deepSanitize(obj: any, dynamicOptions?: any, visited = new WeakSet()): any {
|
|
31
|
+
|
|
32
|
+
// if (obj && typeof obj === "object") {
|
|
33
|
+
// if (visited.has(obj)) {
|
|
34
|
+
// return obj;
|
|
35
|
+
// }
|
|
36
|
+
// visited.add(obj);
|
|
37
|
+
// }
|
|
38
|
+
|
|
39
|
+
// if (typeof obj === "string") {
|
|
40
|
+
// return this.sanitize(obj, dynamicOptions);
|
|
41
|
+
// }
|
|
42
|
+
|
|
43
|
+
// if (Array.isArray(obj)) {
|
|
44
|
+
// return obj.map((item) => this.deepSanitize(item, dynamicOptions, visited));
|
|
45
|
+
// }
|
|
46
|
+
|
|
47
|
+
// if (obj && typeof obj === "object") {
|
|
48
|
+
// const result: any = {};
|
|
49
|
+
// for (const key of Object.keys(obj)) {
|
|
50
|
+
// result[key] = this.deepSanitize(obj[key], dynamicOptions, visited);
|
|
51
|
+
// }
|
|
52
|
+
// return result;
|
|
53
|
+
// }
|
|
54
|
+
|
|
55
|
+
// return obj;
|
|
56
|
+
// }
|
|
57
|
+
|
|
58
|
+
// middleware(dynamicOptions?: any) {
|
|
59
|
+
// return (req: any, _res: any, next: any) => {
|
|
60
|
+
// try {
|
|
61
|
+
// if (req.body) {
|
|
62
|
+
// req.body = this.deepSanitize(req.body, dynamicOptions);
|
|
63
|
+
|
|
64
|
+
// logger.debug("sanitize-html applied", {
|
|
65
|
+
// keys: Object.keys(req.body)
|
|
66
|
+
// });
|
|
67
|
+
// }
|
|
68
|
+
// next();
|
|
69
|
+
|
|
70
|
+
// } catch (err: any) {
|
|
71
|
+
// logger.error("sanitize-html middleware failed", {
|
|
72
|
+
// error: err?.message || err
|
|
73
|
+
// });
|
|
74
|
+
// next(err);
|
|
75
|
+
// }
|
|
76
|
+
// };
|
|
77
|
+
// }
|
|
78
|
+
// }
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
1
84
|
import sanitizeHtml from "sanitize-html";
|
|
2
|
-
import { AdapterError } from "../core/errors/AdapterError
|
|
3
|
-
import { logger } from "../logging
|
|
85
|
+
import { AdapterError } from "../core/errors/AdapterError";
|
|
86
|
+
import { logger } from "../logging";
|
|
4
87
|
|
|
5
88
|
export class SanitizeHtmlAdapter {
|
|
6
89
|
private globalOptions: sanitizeHtml.IOptions;
|
|
@@ -12,27 +95,25 @@ export class SanitizeHtmlAdapter {
|
|
|
12
95
|
sanitize(input: string, dynamicOptions?: any): string {
|
|
13
96
|
try {
|
|
14
97
|
const opts = { ...this.globalOptions, ...(dynamicOptions || {}) };
|
|
15
|
-
|
|
16
98
|
const clean = sanitizeHtml(input, opts);
|
|
99
|
+
|
|
17
100
|
return typeof clean === "string" ? clean : String(clean);
|
|
18
101
|
|
|
19
102
|
} catch (err: any) {
|
|
20
|
-
logger.error("
|
|
21
|
-
|
|
22
|
-
|
|
103
|
+
logger.error("HTML sanitization failed", {
|
|
104
|
+
adapter: "sanitize-html",
|
|
105
|
+
operation: "sanitize",
|
|
106
|
+
reason: err?.message
|
|
23
107
|
});
|
|
24
108
|
|
|
25
109
|
throw new AdapterError("sanitize-html adapter failed.");
|
|
26
110
|
}
|
|
27
111
|
}
|
|
28
112
|
|
|
29
|
-
// Deep Sanitization -
|
|
113
|
+
// Deep Sanitization - recursively
|
|
30
114
|
private deepSanitize(obj: any, dynamicOptions?: any, visited = new WeakSet()): any {
|
|
31
|
-
|
|
32
115
|
if (obj && typeof obj === "object") {
|
|
33
|
-
if (visited.has(obj))
|
|
34
|
-
return obj;
|
|
35
|
-
}
|
|
116
|
+
if (visited.has(obj)) return obj;
|
|
36
117
|
visited.add(obj);
|
|
37
118
|
}
|
|
38
119
|
|
|
@@ -41,13 +122,19 @@ export class SanitizeHtmlAdapter {
|
|
|
41
122
|
}
|
|
42
123
|
|
|
43
124
|
if (Array.isArray(obj)) {
|
|
44
|
-
return obj.map(
|
|
125
|
+
return obj.map(item =>
|
|
126
|
+
this.deepSanitize(item, dynamicOptions, visited)
|
|
127
|
+
);
|
|
45
128
|
}
|
|
46
129
|
|
|
47
130
|
if (obj && typeof obj === "object") {
|
|
48
131
|
const result: any = {};
|
|
49
132
|
for (const key of Object.keys(obj)) {
|
|
50
|
-
result[key] = this.deepSanitize(
|
|
133
|
+
result[key] = this.deepSanitize(
|
|
134
|
+
obj[key],
|
|
135
|
+
dynamicOptions,
|
|
136
|
+
visited
|
|
137
|
+
);
|
|
51
138
|
}
|
|
52
139
|
return result;
|
|
53
140
|
}
|
|
@@ -61,18 +148,24 @@ export class SanitizeHtmlAdapter {
|
|
|
61
148
|
if (req.body) {
|
|
62
149
|
req.body = this.deepSanitize(req.body, dynamicOptions);
|
|
63
150
|
|
|
64
|
-
|
|
151
|
+
// ✅ visible + safe info log
|
|
152
|
+
logger.info("HTML sanitization applied", {
|
|
153
|
+
adapter: "sanitize-html",
|
|
154
|
+
operation: "middleware",
|
|
65
155
|
keys: Object.keys(req.body)
|
|
66
156
|
});
|
|
67
157
|
}
|
|
68
|
-
next();
|
|
69
158
|
|
|
159
|
+
next();
|
|
70
160
|
} catch (err: any) {
|
|
71
|
-
logger.error("
|
|
72
|
-
|
|
161
|
+
logger.error("HTML sanitization middleware failed", {
|
|
162
|
+
adapter: "sanitize-html",
|
|
163
|
+
operation: "middleware",
|
|
164
|
+
reason: err?.message
|
|
73
165
|
});
|
|
166
|
+
|
|
74
167
|
next(err);
|
|
75
168
|
}
|
|
76
169
|
};
|
|
77
170
|
}
|
|
78
|
-
}
|
|
171
|
+
}
|
|
@@ -1,6 +1,155 @@
|
|
|
1
|
-
import { FilterXSS, getDefaultWhiteList, whiteList } from 'xss';
|
|
2
|
-
import { AdapterError } from "../core/errors/AdapterError.js";
|
|
3
|
-
import { logger } from "../logging/index.js";
|
|
1
|
+
// import { FilterXSS, getDefaultWhiteList, whiteList } from 'xss';
|
|
2
|
+
// import { AdapterError } from "../core/errors/AdapterError.js";
|
|
3
|
+
// import { logger } from "../logging/index.js";
|
|
4
|
+
|
|
5
|
+
// export interface XSSOptions {
|
|
6
|
+
// whiteList?: typeof whiteList;
|
|
7
|
+
// stripIgnoreTag?: boolean;
|
|
8
|
+
// stripIgnoreTagBody?: string[];
|
|
9
|
+
// allowCommentTag?: boolean;
|
|
10
|
+
// css?: boolean | { [key: string]: boolean };
|
|
11
|
+
// onTag?: (tag: string, html: string, options: any) => string;
|
|
12
|
+
// onTagAttr?: (tag: string, name: string, value: string, isWhiteAttr: boolean) => string;
|
|
13
|
+
// onIgnoreTag?: (tag: string, html: string, options: any) => string;
|
|
14
|
+
// [key: string]: any;
|
|
15
|
+
// }
|
|
16
|
+
|
|
17
|
+
// export class XSSAdapter {
|
|
18
|
+
// private globalOptions: XSSOptions;
|
|
19
|
+
// private defaultFilter: FilterXSS;
|
|
20
|
+
|
|
21
|
+
// constructor(options: XSSOptions = {}) {
|
|
22
|
+
// this.globalOptions = options;
|
|
23
|
+
|
|
24
|
+
// // Default safe configuration
|
|
25
|
+
// const defaultOptions: XSSOptions = {
|
|
26
|
+
// whiteList: getDefaultWhiteList(),
|
|
27
|
+
// stripIgnoreTag: true,
|
|
28
|
+
// stripIgnoreTagBody: ['script', 'style', 'iframe', 'object', 'embed'],
|
|
29
|
+
// allowCommentTag: false,
|
|
30
|
+
// css: false,
|
|
31
|
+
// onTag: (tag, html, options) => {
|
|
32
|
+
|
|
33
|
+
// if (tag === 'a') {
|
|
34
|
+
// return html.replace(/<a /i, '<a target="_blank" rel="noopener noreferrer" ');
|
|
35
|
+
// }
|
|
36
|
+
// return html;
|
|
37
|
+
// }
|
|
38
|
+
// };
|
|
39
|
+
|
|
40
|
+
// const finalOptions = { ...defaultOptions, ...options };
|
|
41
|
+
// this.defaultFilter = new FilterXSS(finalOptions);
|
|
42
|
+
// }
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
// sanitize(input: string, dynamicOptions?: XSSOptions): string {
|
|
46
|
+
// try {
|
|
47
|
+
// if (typeof input !== "string") {
|
|
48
|
+
// return input as any;
|
|
49
|
+
// }
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
// if (!dynamicOptions || Object.keys(dynamicOptions).length === 0) {
|
|
53
|
+
// return this.defaultFilter.process(input);
|
|
54
|
+
// }
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
// const mergedOptions = { ...this.globalOptions, ...dynamicOptions };
|
|
58
|
+
// const customFilter = new FilterXSS(mergedOptions);
|
|
59
|
+
|
|
60
|
+
// return customFilter.process(input);
|
|
61
|
+
|
|
62
|
+
// } catch (err: any) {
|
|
63
|
+
// logger.error("XSS sanitizer failed", {
|
|
64
|
+
// error: err?.message,
|
|
65
|
+
// preview: input?.slice?.(0, 80)
|
|
66
|
+
// });
|
|
67
|
+
// throw new AdapterError("XSS sanitizer failed.");
|
|
68
|
+
// }
|
|
69
|
+
// }
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// middleware(dynamicOptions?: XSSOptions) {
|
|
73
|
+
// return (req: any, _res: any, next: any) => {
|
|
74
|
+
// try {
|
|
75
|
+
// if (req.body && typeof req.body === "object") {
|
|
76
|
+
// const originalBody = req.body;
|
|
77
|
+
// const sanitizedBody: any = Array.isArray(originalBody) ? [] : {};
|
|
78
|
+
|
|
79
|
+
// for (const key of Object.keys(originalBody)) {
|
|
80
|
+
// const val = originalBody[key];
|
|
81
|
+
|
|
82
|
+
// if (typeof val === "string") {
|
|
83
|
+
// sanitizedBody[key] = this.sanitize(val, dynamicOptions);
|
|
84
|
+
// } else if (Array.isArray(val)) {
|
|
85
|
+
// sanitizedBody[key] = val.map((v) =>
|
|
86
|
+
// typeof v === "string"
|
|
87
|
+
// ? this.sanitize(v, dynamicOptions)
|
|
88
|
+
// : v
|
|
89
|
+
// );
|
|
90
|
+
// } else if (val && typeof val === "object") {
|
|
91
|
+
|
|
92
|
+
// sanitizedBody[key] = this.deepSanitize(val, dynamicOptions);
|
|
93
|
+
// } else {
|
|
94
|
+
// sanitizedBody[key] = val;
|
|
95
|
+
// }
|
|
96
|
+
// }
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
// req.sanitizedBody = sanitizedBody;
|
|
100
|
+
|
|
101
|
+
// logger.debug("XSS sanitizer applied", {
|
|
102
|
+
// originalKeys: Object.keys(originalBody),
|
|
103
|
+
// sanitizedKeys: Object.keys(sanitizedBody)
|
|
104
|
+
// });
|
|
105
|
+
// }
|
|
106
|
+
|
|
107
|
+
// next();
|
|
108
|
+
// } catch (err: any) {
|
|
109
|
+
// logger.error("XSS middleware failed", {
|
|
110
|
+
// error: err?.message || err
|
|
111
|
+
// });
|
|
112
|
+
// next(err);
|
|
113
|
+
// }
|
|
114
|
+
// };
|
|
115
|
+
// }
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
// private deepSanitize(obj: any, options?: XSSOptions, visited = new WeakSet()): any {
|
|
119
|
+
|
|
120
|
+
// if (obj && typeof obj === "object") {
|
|
121
|
+
// if (visited.has(obj)) {
|
|
122
|
+
// return obj;
|
|
123
|
+
// }
|
|
124
|
+
// visited.add(obj);
|
|
125
|
+
// }
|
|
126
|
+
|
|
127
|
+
// if (typeof obj === "string") {
|
|
128
|
+
// return this.sanitize(obj, options);
|
|
129
|
+
// }
|
|
130
|
+
|
|
131
|
+
// if (Array.isArray(obj)) {
|
|
132
|
+
// return obj.map(item => this.deepSanitize(item, options, visited));
|
|
133
|
+
// }
|
|
134
|
+
|
|
135
|
+
// if (obj && typeof obj === "object") {
|
|
136
|
+
// const result: any = {};
|
|
137
|
+
// for (const key of Object.keys(obj)) {
|
|
138
|
+
// result[key] = this.deepSanitize(obj[key], options, visited);
|
|
139
|
+
// }
|
|
140
|
+
// return result;
|
|
141
|
+
// }
|
|
142
|
+
|
|
143
|
+
// return obj;
|
|
144
|
+
// }
|
|
145
|
+
// }
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
import { FilterXSS, getDefaultWhiteList, whiteList } from "xss";
|
|
151
|
+
import { AdapterError } from "../core/errors/AdapterError";
|
|
152
|
+
import { logger } from "../logging";
|
|
4
153
|
|
|
5
154
|
export interface XSSOptions {
|
|
6
155
|
whiteList?: typeof whiteList;
|
|
@@ -20,18 +169,19 @@ export class XSSAdapter {
|
|
|
20
169
|
|
|
21
170
|
constructor(options: XSSOptions = {}) {
|
|
22
171
|
this.globalOptions = options;
|
|
23
|
-
|
|
24
|
-
// Default safe configuration
|
|
172
|
+
|
|
25
173
|
const defaultOptions: XSSOptions = {
|
|
26
174
|
whiteList: getDefaultWhiteList(),
|
|
27
|
-
stripIgnoreTag: true,
|
|
28
|
-
stripIgnoreTagBody: [
|
|
175
|
+
stripIgnoreTag: true,
|
|
176
|
+
stripIgnoreTagBody: ["script", "style", "iframe", "object", "embed"],
|
|
29
177
|
allowCommentTag: false,
|
|
30
|
-
css: false,
|
|
31
|
-
onTag: (tag, html
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
178
|
+
css: false,
|
|
179
|
+
onTag: (tag, html) => {
|
|
180
|
+
if (tag === "a") {
|
|
181
|
+
return html.replace(
|
|
182
|
+
/<a /i,
|
|
183
|
+
'<a target="_blank" rel="noopener noreferrer" '
|
|
184
|
+
);
|
|
35
185
|
}
|
|
36
186
|
return html;
|
|
37
187
|
}
|
|
@@ -41,86 +191,80 @@ export class XSSAdapter {
|
|
|
41
191
|
this.defaultFilter = new FilterXSS(finalOptions);
|
|
42
192
|
}
|
|
43
193
|
|
|
44
|
-
|
|
45
194
|
sanitize(input: string, dynamicOptions?: XSSOptions): string {
|
|
46
195
|
try {
|
|
47
|
-
if (typeof input !== "string")
|
|
48
|
-
return input as any;
|
|
49
|
-
}
|
|
196
|
+
if (typeof input !== "string") return input as any;
|
|
50
197
|
|
|
51
|
-
|
|
52
198
|
if (!dynamicOptions || Object.keys(dynamicOptions).length === 0) {
|
|
53
199
|
return this.defaultFilter.process(input);
|
|
54
200
|
}
|
|
55
201
|
|
|
56
|
-
|
|
57
202
|
const mergedOptions = { ...this.globalOptions, ...dynamicOptions };
|
|
58
203
|
const customFilter = new FilterXSS(mergedOptions);
|
|
59
|
-
|
|
204
|
+
|
|
60
205
|
return customFilter.process(input);
|
|
61
206
|
|
|
62
207
|
} catch (err: any) {
|
|
63
|
-
logger.error("XSS
|
|
64
|
-
|
|
65
|
-
|
|
208
|
+
logger.error("XSS sanitization failed", {
|
|
209
|
+
adapter: "xss",
|
|
210
|
+
operation: "sanitize",
|
|
211
|
+
reason: err?.message
|
|
66
212
|
});
|
|
213
|
+
|
|
67
214
|
throw new AdapterError("XSS sanitizer failed.");
|
|
68
215
|
}
|
|
69
216
|
}
|
|
70
217
|
|
|
71
|
-
|
|
72
218
|
middleware(dynamicOptions?: XSSOptions) {
|
|
73
219
|
return (req: any, _res: any, next: any) => {
|
|
74
220
|
try {
|
|
75
221
|
if (req.body && typeof req.body === "object") {
|
|
76
222
|
const originalBody = req.body;
|
|
77
223
|
const sanitizedBody: any = Array.isArray(originalBody) ? [] : {};
|
|
78
|
-
|
|
224
|
+
|
|
79
225
|
for (const key of Object.keys(originalBody)) {
|
|
80
226
|
const val = originalBody[key];
|
|
81
227
|
|
|
82
228
|
if (typeof val === "string") {
|
|
83
229
|
sanitizedBody[key] = this.sanitize(val, dynamicOptions);
|
|
84
230
|
} else if (Array.isArray(val)) {
|
|
85
|
-
sanitizedBody[key] = val.map(
|
|
231
|
+
sanitizedBody[key] = val.map(v =>
|
|
86
232
|
typeof v === "string"
|
|
87
233
|
? this.sanitize(v, dynamicOptions)
|
|
88
234
|
: v
|
|
89
235
|
);
|
|
90
236
|
} else if (val && typeof val === "object") {
|
|
91
|
-
|
|
92
237
|
sanitizedBody[key] = this.deepSanitize(val, dynamicOptions);
|
|
93
238
|
} else {
|
|
94
239
|
sanitizedBody[key] = val;
|
|
95
240
|
}
|
|
96
241
|
}
|
|
97
|
-
|
|
98
|
-
|
|
242
|
+
|
|
99
243
|
req.sanitizedBody = sanitizedBody;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
244
|
+
|
|
245
|
+
// ✅ visible + safe info log
|
|
246
|
+
logger.info("XSS sanitization applied", {
|
|
247
|
+
adapter: "xss",
|
|
248
|
+
operation: "middleware",
|
|
249
|
+
keys: Object.keys(sanitizedBody)
|
|
104
250
|
});
|
|
105
251
|
}
|
|
106
252
|
|
|
107
253
|
next();
|
|
108
254
|
} catch (err: any) {
|
|
109
255
|
logger.error("XSS middleware failed", {
|
|
110
|
-
|
|
256
|
+
adapter: "xss",
|
|
257
|
+
operation: "middleware",
|
|
258
|
+
reason: err?.message
|
|
111
259
|
});
|
|
112
260
|
next(err);
|
|
113
261
|
}
|
|
114
262
|
};
|
|
115
263
|
}
|
|
116
264
|
|
|
117
|
-
|
|
118
265
|
private deepSanitize(obj: any, options?: XSSOptions, visited = new WeakSet()): any {
|
|
119
|
-
|
|
120
266
|
if (obj && typeof obj === "object") {
|
|
121
|
-
if (visited.has(obj))
|
|
122
|
-
return obj;
|
|
123
|
-
}
|
|
267
|
+
if (visited.has(obj)) return obj;
|
|
124
268
|
visited.add(obj);
|
|
125
269
|
}
|
|
126
270
|
|
|
@@ -142,4 +286,4 @@ export class XSSAdapter {
|
|
|
142
286
|
|
|
143
287
|
return obj;
|
|
144
288
|
}
|
|
145
|
-
}
|
|
289
|
+
}
|