hi-secure 1.0.15 → 1.0.17
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 +7 -5
- package/dist/adapters/ArgonAdapter.js.map +1 -1
- package/dist/adapters/BcryptAdapter.d.ts.map +1 -1
- package/dist/adapters/BcryptAdapter.js +7 -3
- package/dist/adapters/BcryptAdapter.js.map +1 -1
- package/dist/adapters/ExpressRLAdapter.d.ts.map +1 -1
- package/dist/adapters/ExpressRLAdapter.js +10 -6
- package/dist/adapters/ExpressRLAdapter.js.map +1 -1
- package/dist/adapters/ExpressValidatorAdapter.d.ts.map +1 -1
- package/dist/adapters/ExpressValidatorAdapter.js +14 -10
- package/dist/adapters/ExpressValidatorAdapter.js.map +1 -1
- package/dist/adapters/GoogleAdapter.d.ts.map +1 -1
- package/dist/adapters/GoogleAdapter.js +19 -16
- package/dist/adapters/GoogleAdapter.js.map +1 -1
- package/dist/adapters/JWTAdapter.d.ts.map +1 -1
- package/dist/adapters/JWTAdapter.js +25 -15
- package/dist/adapters/JWTAdapter.js.map +1 -1
- package/dist/adapters/RLFlexibleAdapter.d.ts.map +1 -1
- package/dist/adapters/RLFlexibleAdapter.js +23 -12
- package/dist/adapters/RLFlexibleAdapter.js.map +1 -1
- package/dist/adapters/SanitizeHtmlAdapter.d.ts.map +1 -1
- package/dist/adapters/SanitizeHtmlAdapter.js +17 -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 +21 -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 +10 -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 +91 -120
- 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/morganSetup.d.ts.map +1 -1
- package/dist/logging/morganSetup.js +8 -1
- package/dist/logging/morganSetup.js.map +1 -1
- package/dist/logging/winstonSetup.d.ts.map +1 -1
- package/dist/logging/winstonSetup.js +17 -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 +59 -31
- package/dist/managers/AuthManager.js.map +1 -1
- package/dist/managers/CorsManager.d.ts.map +1 -1
- package/dist/managers/CorsManager.js +18 -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 +35 -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 +44 -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 +43 -22
- package/dist/managers/RateLimitManager.js.map +1 -1
- package/dist/managers/SanitizerManager.d.ts.map +1 -1
- package/dist/managers/SanitizerManager.js +32 -15
- package/dist/managers/SanitizerManager.js.map +1 -1
- package/dist/managers/ValidatorManager.d.ts.map +1 -1
- package/dist/managers/ValidatorManager.js +31 -7
- package/dist/managers/ValidatorManager.js.map +1 -1
- package/package.json +2 -6
- package/readme.md +3 -6
- package/src/adapters/ArgonAdapter.ts +10 -6
- package/src/adapters/BcryptAdapter.ts +7 -8
- package/src/adapters/ExpressRLAdapter.ts +14 -9
- package/src/adapters/ExpressValidatorAdapter.ts +17 -11
- package/src/adapters/GoogleAdapter.ts +24 -21
- package/src/adapters/JWTAdapter.ts +33 -21
- package/src/adapters/RLFlexibleAdapter.ts +31 -16
- package/src/adapters/SanitizeHtmlAdapter.ts +28 -18
- package/src/adapters/XSSAdapter.ts +33 -38
- package/src/adapters/ZodAdapter.ts +10 -10
- package/src/core/HiSecure.ts +127 -161
- package/src/index.ts +4 -0
- package/src/logging/morganSetup.ts +11 -1
- package/src/logging/winstonSetup.ts +35 -8
- package/src/managers/AuthManager.ts +64 -34
- package/src/managers/CorsManager.ts +23 -16
- package/src/managers/HashManager.ts +48 -19
- package/src/managers/JsonManager.ts +57 -15
- package/src/managers/RateLimitManager.ts +61 -29
- package/src/managers/SanitizerManager.ts +47 -25
- package/src/managers/ValidatorManager.ts +40 -15
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { validationResult } from "express-validator";
|
|
2
|
-
import { ValidationError } from "../core/errors/ValidationError
|
|
3
|
-
import { logger } from "../logging
|
|
2
|
+
import { ValidationError } from "../core/errors/ValidationError";
|
|
3
|
+
import { logger } from "../logging";
|
|
4
4
|
|
|
5
5
|
export class ExpressValidatorAdapter {
|
|
6
6
|
private globalSchema?: any[];
|
|
@@ -23,24 +23,30 @@ export class ExpressValidatorAdapter {
|
|
|
23
23
|
const errors = validationResult(req);
|
|
24
24
|
|
|
25
25
|
if (!errors.isEmpty()) {
|
|
26
|
-
const
|
|
26
|
+
const formattedErrors = errors.array().map(err => ({
|
|
27
27
|
message: err.msg,
|
|
28
|
-
|
|
29
|
-
// location: err.location
|
|
28
|
+
field: err.type
|
|
30
29
|
}));
|
|
31
30
|
|
|
32
|
-
logger.warn("
|
|
33
|
-
|
|
31
|
+
logger.warn("Request validation failed", {
|
|
32
|
+
adapter: "express-validator",
|
|
33
|
+
operation: "validate",
|
|
34
34
|
method: req.method,
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
path: req.path,
|
|
36
|
+
errorCount: formattedErrors.length,
|
|
37
|
+
errors: formattedErrors,
|
|
38
|
+
bodyPreview: req.body
|
|
39
|
+
? JSON.stringify(req.body).slice(0, 150)
|
|
40
|
+
: undefined
|
|
37
41
|
});
|
|
38
42
|
|
|
39
|
-
return next(
|
|
43
|
+
return next(
|
|
44
|
+
new ValidationError("Validation failed.", formattedErrors as any)
|
|
45
|
+
);
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
next();
|
|
43
49
|
}
|
|
44
50
|
];
|
|
45
51
|
}
|
|
46
|
-
}
|
|
52
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { OAuth2Client, LoginTicket } from "google-auth-library";
|
|
2
|
-
import { AdapterError } from "../core/errors/AdapterError
|
|
3
|
-
|
|
4
|
-
import {logger} from '../logging';
|
|
2
|
+
import { AdapterError } from "../core/errors/AdapterError";
|
|
3
|
+
import { logger } from "../logging";
|
|
5
4
|
|
|
6
5
|
export interface GoogleTokenPayload {
|
|
7
6
|
sub: string;
|
|
@@ -20,60 +19,64 @@ export class GoogleAdapter {
|
|
|
20
19
|
if (clientId && clientId.trim().length === 0) {
|
|
21
20
|
throw new AdapterError("Google clientId cannot be empty string");
|
|
22
21
|
}
|
|
23
|
-
|
|
22
|
+
|
|
24
23
|
this.client = new OAuth2Client(clientId);
|
|
25
24
|
this.clientId = clientId;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
async verifyIdToken(idToken: string): Promise<GoogleTokenPayload> {
|
|
29
28
|
try {
|
|
30
|
-
if (!idToken || typeof idToken !==
|
|
29
|
+
if (!idToken || typeof idToken !== "string") {
|
|
31
30
|
throw new AdapterError("Invalid ID token provided");
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
const options: { idToken: string; audience?: string | string[] } = {
|
|
35
|
-
idToken
|
|
33
|
+
const options: { idToken: string; audience?: string | string[] } = {
|
|
34
|
+
idToken
|
|
36
35
|
};
|
|
37
36
|
|
|
38
|
-
// audience only if clientId is provided and not empty
|
|
39
37
|
if (this.clientId && this.clientId.trim().length > 0) {
|
|
40
38
|
options.audience = this.clientId;
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
const ticket: LoginTicket = await this.client.verifyIdToken(options);
|
|
44
42
|
const payload = ticket.getPayload();
|
|
45
|
-
|
|
43
|
+
|
|
46
44
|
if (!payload) {
|
|
47
|
-
logger.warn("
|
|
45
|
+
logger.warn("Google ID token payload empty", {
|
|
46
|
+
adapter: "google-auth",
|
|
47
|
+
operation: "verifyIdToken",
|
|
48
|
+
hasClientId: !!this.clientId
|
|
49
|
+
});
|
|
50
|
+
|
|
48
51
|
throw new AdapterError("Invalid Google ID token payload.");
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
// result object
|
|
52
54
|
const result: GoogleTokenPayload = {
|
|
53
55
|
sub: payload.sub,
|
|
54
|
-
email: payload.email ||
|
|
56
|
+
email: payload.email || "",
|
|
55
57
|
email_verified: payload.email_verified || false,
|
|
56
58
|
name: payload.name,
|
|
57
59
|
picture: payload.picture
|
|
58
60
|
};
|
|
59
61
|
|
|
60
|
-
// remaining properties from payload
|
|
61
62
|
const { sub, email, email_verified, name, picture, ...rest } = payload;
|
|
62
63
|
Object.assign(result, rest);
|
|
63
64
|
|
|
64
65
|
return result;
|
|
65
66
|
|
|
66
67
|
} catch (err: any) {
|
|
67
|
-
logger.error("
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
logger.error("Google ID token verification failed", {
|
|
69
|
+
adapter: "google-auth",
|
|
70
|
+
operation: "verifyIdToken",
|
|
71
|
+
hasClientId: !!this.clientId,
|
|
72
|
+
reason: err?.message
|
|
70
73
|
});
|
|
71
|
-
|
|
72
|
-
if (err
|
|
74
|
+
|
|
75
|
+
if (err?.message?.includes("audience")) {
|
|
73
76
|
throw new AdapterError("Invalid Google client ID configured.");
|
|
74
77
|
}
|
|
75
|
-
|
|
76
|
-
throw new AdapterError(
|
|
78
|
+
|
|
79
|
+
throw new AdapterError("Google token verification failed.");
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
|
-
}
|
|
82
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import jwt from "jsonwebtoken";
|
|
2
|
-
import { randomUUID } from "crypto";
|
|
3
|
-
import { AdapterError } from "../core/errors/AdapterError
|
|
4
|
-
import { logError } from "../logging/index.js";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { AdapterError } from "../core/errors/AdapterError";
|
|
5
4
|
import { logger } from "../logging";
|
|
6
5
|
|
|
7
6
|
export interface JWTAdapterOptions {
|
|
@@ -14,7 +13,7 @@ export interface JWTAdapterOptions {
|
|
|
14
13
|
|
|
15
14
|
export interface SignOptions {
|
|
16
15
|
expiresIn?: string | number;
|
|
17
|
-
jti?: string;
|
|
16
|
+
jti?: string;
|
|
18
17
|
subject?: string;
|
|
19
18
|
issuer?: string;
|
|
20
19
|
audience?: string | string[];
|
|
@@ -33,13 +32,16 @@ export class JWTAdapter {
|
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
if (options.secret.length < 32) {
|
|
36
|
-
logger.warn("JWT secret
|
|
37
|
-
|
|
35
|
+
logger.warn("Weak JWT secret detected", {
|
|
36
|
+
adapter: "jwt",
|
|
37
|
+
operation: "init",
|
|
38
|
+
secretLength: options.secret.length
|
|
39
|
+
});
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
this.secret = options.secret;
|
|
41
43
|
this.expiresIn = options.expiresIn;
|
|
42
|
-
this.algorithm = options.algorithm ||
|
|
44
|
+
this.algorithm = options.algorithm || "HS256";
|
|
43
45
|
this.issuer = options.issuer;
|
|
44
46
|
this.audience = options.audience;
|
|
45
47
|
}
|
|
@@ -50,21 +52,26 @@ export class JWTAdapter {
|
|
|
50
52
|
algorithm: this.algorithm,
|
|
51
53
|
issuer: options?.issuer || this.issuer,
|
|
52
54
|
audience: options?.audience || this.audience,
|
|
53
|
-
jwtid: options?.jti || randomUUID(),
|
|
55
|
+
jwtid: options?.jti || randomUUID(),
|
|
54
56
|
subject: options?.subject
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
if (options?.expiresIn !== undefined) {
|
|
58
|
-
jwtOptions.expiresIn = options.expiresIn as
|
|
60
|
+
jwtOptions.expiresIn = options.expiresIn as any;
|
|
59
61
|
} else if (this.expiresIn !== undefined) {
|
|
60
|
-
jwtOptions.expiresIn = this.expiresIn as
|
|
62
|
+
jwtOptions.expiresIn = this.expiresIn as any;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
return jwt.sign(payload, this.secret, jwtOptions);
|
|
64
66
|
|
|
65
67
|
} catch (err: any) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
logger.error("JWT signing failed", {
|
|
69
|
+
adapter: "jwt",
|
|
70
|
+
operation: "sign",
|
|
71
|
+
reason: err?.message
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
throw new AdapterError("JWT sign failed");
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
77
|
|
|
@@ -73,22 +80,27 @@ export class JWTAdapter {
|
|
|
73
80
|
const verifyOptions: jwt.VerifyOptions = {
|
|
74
81
|
algorithms: [this.algorithm],
|
|
75
82
|
issuer: this.issuer,
|
|
76
|
-
audience: options?.audience
|
|
83
|
+
audience: (options?.audience || this.audience) as string
|
|
77
84
|
};
|
|
78
85
|
|
|
79
86
|
return jwt.verify(token, this.secret, verifyOptions);
|
|
87
|
+
|
|
80
88
|
} catch (err: any) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
logger.error("JWT verification failed", {
|
|
90
|
+
adapter: "jwt",
|
|
91
|
+
operation: "verify",
|
|
92
|
+
reason: err?.message
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (err?.name === "TokenExpiredError") {
|
|
85
96
|
throw new AdapterError("JWT token has expired");
|
|
86
97
|
}
|
|
87
|
-
|
|
98
|
+
|
|
99
|
+
if (err?.name === "JsonWebTokenError") {
|
|
88
100
|
throw new AdapterError("Invalid JWT token");
|
|
89
101
|
}
|
|
90
|
-
|
|
91
|
-
throw new AdapterError(
|
|
102
|
+
|
|
103
|
+
throw new AdapterError("JWT verification failed");
|
|
92
104
|
}
|
|
93
105
|
}
|
|
94
|
-
}
|
|
106
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { RateLimiterMemory, RateLimiterRes } from "rate-limiter-flexible";
|
|
2
|
-
import { logger } from "../logging
|
|
3
|
-
import { AdapterError } from "../core/errors/AdapterError
|
|
2
|
+
import { logger } from "../logging";
|
|
3
|
+
import { AdapterError } from "../core/errors/AdapterError";
|
|
4
4
|
|
|
5
5
|
export interface RLOptions {
|
|
6
6
|
points?: number;
|
|
7
|
-
duration?: number;
|
|
7
|
+
duration?: number;
|
|
8
8
|
message?: any;
|
|
9
9
|
blockDuration?: number;
|
|
10
10
|
}
|
|
@@ -27,8 +27,16 @@ export class RLFlexibleAdapter {
|
|
|
27
27
|
blockDuration: finalOptions.blockDuration
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
|
|
31
|
+
logger.info("Rate limiter initialized", {
|
|
32
|
+
adapter: "rate-limiter-flexible",
|
|
33
|
+
operation: "init",
|
|
34
|
+
points: finalOptions.points,
|
|
35
|
+
duration: finalOptions.duration,
|
|
36
|
+
blockDuration: finalOptions.blockDuration
|
|
37
|
+
});
|
|
38
|
+
|
|
30
39
|
return async (req: any, res: any, next: any) => {
|
|
31
|
-
|
|
32
40
|
const ip = this.extractIP(req);
|
|
33
41
|
|
|
34
42
|
try {
|
|
@@ -37,15 +45,20 @@ export class RLFlexibleAdapter {
|
|
|
37
45
|
} catch (err: any) {
|
|
38
46
|
const rlErr = err as RateLimiterRes;
|
|
39
47
|
|
|
40
|
-
logger.warn("
|
|
48
|
+
logger.warn("Rate limit exceeded", {
|
|
49
|
+
adapter: "rate-limiter-flexible",
|
|
50
|
+
operation: "consume",
|
|
41
51
|
ip,
|
|
42
|
-
path: req.path,
|
|
43
52
|
method: req.method,
|
|
44
|
-
|
|
53
|
+
path: req.path,
|
|
54
|
+
retryAfterMs: rlErr.msBeforeNext
|
|
45
55
|
});
|
|
46
56
|
|
|
47
|
-
res.setHeader(
|
|
48
|
-
|
|
57
|
+
res.setHeader(
|
|
58
|
+
"Retry-After",
|
|
59
|
+
Math.ceil(rlErr.msBeforeNext / 1000)
|
|
60
|
+
);
|
|
61
|
+
|
|
49
62
|
return res.status(429).json({
|
|
50
63
|
success: false,
|
|
51
64
|
error: "RATE_LIMIT_EXCEEDED",
|
|
@@ -54,23 +67,25 @@ export class RLFlexibleAdapter {
|
|
|
54
67
|
});
|
|
55
68
|
}
|
|
56
69
|
};
|
|
57
|
-
|
|
58
70
|
} catch (err: any) {
|
|
59
|
-
logger.error("
|
|
60
|
-
|
|
71
|
+
logger.error("Rate limiter initialization failed", {
|
|
72
|
+
adapter: "rate-limiter-flexible",
|
|
73
|
+
operation: "init",
|
|
74
|
+
reason: err?.message
|
|
61
75
|
});
|
|
76
|
+
|
|
62
77
|
throw new AdapterError("RateLimiterFlexible creation failed.");
|
|
63
78
|
}
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
private extractIP(req: any): string {
|
|
67
82
|
return (
|
|
68
|
-
req.headers[
|
|
69
|
-
req.headers[
|
|
83
|
+
req.headers["x-real-ip"] ||
|
|
84
|
+
req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
|
|
70
85
|
req.ip ||
|
|
71
86
|
req.connection?.remoteAddress ||
|
|
72
87
|
req.socket?.remoteAddress ||
|
|
73
|
-
|
|
88
|
+
"unknown"
|
|
74
89
|
);
|
|
75
90
|
}
|
|
76
|
-
}
|
|
91
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import sanitizeHtml from "sanitize-html";
|
|
2
|
-
import { AdapterError } from "../core/errors/AdapterError
|
|
3
|
-
import { logger } from "../logging
|
|
2
|
+
import { AdapterError } from "../core/errors/AdapterError";
|
|
3
|
+
import { logger } from "../logging";
|
|
4
4
|
|
|
5
5
|
export class SanitizeHtmlAdapter {
|
|
6
6
|
private globalOptions: sanitizeHtml.IOptions;
|
|
@@ -12,27 +12,25 @@ export class SanitizeHtmlAdapter {
|
|
|
12
12
|
sanitize(input: string, dynamicOptions?: any): string {
|
|
13
13
|
try {
|
|
14
14
|
const opts = { ...this.globalOptions, ...(dynamicOptions || {}) };
|
|
15
|
-
|
|
16
15
|
const clean = sanitizeHtml(input, opts);
|
|
16
|
+
|
|
17
17
|
return typeof clean === "string" ? clean : String(clean);
|
|
18
18
|
|
|
19
19
|
} catch (err: any) {
|
|
20
|
-
logger.error("
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
logger.error("HTML sanitization failed", {
|
|
21
|
+
adapter: "sanitize-html",
|
|
22
|
+
operation: "sanitize",
|
|
23
|
+
reason: err?.message
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
throw new AdapterError("sanitize-html adapter failed.");
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
// Deep Sanitization -
|
|
30
|
+
// Deep Sanitization - recursively
|
|
30
31
|
private deepSanitize(obj: any, dynamicOptions?: any, visited = new WeakSet()): any {
|
|
31
|
-
|
|
32
32
|
if (obj && typeof obj === "object") {
|
|
33
|
-
if (visited.has(obj))
|
|
34
|
-
return obj;
|
|
35
|
-
}
|
|
33
|
+
if (visited.has(obj)) return obj;
|
|
36
34
|
visited.add(obj);
|
|
37
35
|
}
|
|
38
36
|
|
|
@@ -41,13 +39,19 @@ export class SanitizeHtmlAdapter {
|
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
if (Array.isArray(obj)) {
|
|
44
|
-
return obj.map(
|
|
42
|
+
return obj.map(item =>
|
|
43
|
+
this.deepSanitize(item, dynamicOptions, visited)
|
|
44
|
+
);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
if (obj && typeof obj === "object") {
|
|
48
48
|
const result: any = {};
|
|
49
49
|
for (const key of Object.keys(obj)) {
|
|
50
|
-
result[key] = this.deepSanitize(
|
|
50
|
+
result[key] = this.deepSanitize(
|
|
51
|
+
obj[key],
|
|
52
|
+
dynamicOptions,
|
|
53
|
+
visited
|
|
54
|
+
);
|
|
51
55
|
}
|
|
52
56
|
return result;
|
|
53
57
|
}
|
|
@@ -61,18 +65,24 @@ export class SanitizeHtmlAdapter {
|
|
|
61
65
|
if (req.body) {
|
|
62
66
|
req.body = this.deepSanitize(req.body, dynamicOptions);
|
|
63
67
|
|
|
64
|
-
|
|
68
|
+
|
|
69
|
+
logger.info("HTML sanitization applied", {
|
|
70
|
+
adapter: "sanitize-html",
|
|
71
|
+
operation: "middleware",
|
|
65
72
|
keys: Object.keys(req.body)
|
|
66
73
|
});
|
|
67
74
|
}
|
|
68
|
-
next();
|
|
69
75
|
|
|
76
|
+
next();
|
|
70
77
|
} catch (err: any) {
|
|
71
|
-
logger.error("
|
|
72
|
-
|
|
78
|
+
logger.error("HTML sanitization middleware failed", {
|
|
79
|
+
adapter: "sanitize-html",
|
|
80
|
+
operation: "middleware",
|
|
81
|
+
reason: err?.message
|
|
73
82
|
});
|
|
83
|
+
|
|
74
84
|
next(err);
|
|
75
85
|
}
|
|
76
86
|
};
|
|
77
87
|
}
|
|
78
|
-
}
|
|
88
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { FilterXSS, getDefaultWhiteList, whiteList } from
|
|
2
|
-
import { AdapterError } from "../core/errors/AdapterError
|
|
3
|
-
import { logger } from "../logging
|
|
1
|
+
import { FilterXSS, getDefaultWhiteList, whiteList } from "xss";
|
|
2
|
+
import { AdapterError } from "../core/errors/AdapterError";
|
|
3
|
+
import { logger } from "../logging";
|
|
4
4
|
|
|
5
5
|
export interface XSSOptions {
|
|
6
6
|
whiteList?: typeof whiteList;
|
|
@@ -20,18 +20,19 @@ export class XSSAdapter {
|
|
|
20
20
|
|
|
21
21
|
constructor(options: XSSOptions = {}) {
|
|
22
22
|
this.globalOptions = options;
|
|
23
|
-
|
|
24
|
-
// Default safe configuration
|
|
23
|
+
|
|
25
24
|
const defaultOptions: XSSOptions = {
|
|
26
25
|
whiteList: getDefaultWhiteList(),
|
|
27
|
-
stripIgnoreTag: true,
|
|
28
|
-
stripIgnoreTagBody: [
|
|
26
|
+
stripIgnoreTag: true,
|
|
27
|
+
stripIgnoreTagBody: ["script", "style", "iframe", "object", "embed"],
|
|
29
28
|
allowCommentTag: false,
|
|
30
|
-
css: false,
|
|
31
|
-
onTag: (tag, html
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
css: false,
|
|
30
|
+
onTag: (tag, html) => {
|
|
31
|
+
if (tag === "a") {
|
|
32
|
+
return html.replace(
|
|
33
|
+
/<a /i,
|
|
34
|
+
'<a target="_blank" rel="noopener noreferrer" '
|
|
35
|
+
);
|
|
35
36
|
}
|
|
36
37
|
return html;
|
|
37
38
|
}
|
|
@@ -41,86 +42,80 @@ export class XSSAdapter {
|
|
|
41
42
|
this.defaultFilter = new FilterXSS(finalOptions);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
45
|
sanitize(input: string, dynamicOptions?: XSSOptions): string {
|
|
46
46
|
try {
|
|
47
|
-
if (typeof input !== "string")
|
|
48
|
-
return input as any;
|
|
49
|
-
}
|
|
47
|
+
if (typeof input !== "string") return input as any;
|
|
50
48
|
|
|
51
|
-
|
|
52
49
|
if (!dynamicOptions || Object.keys(dynamicOptions).length === 0) {
|
|
53
50
|
return this.defaultFilter.process(input);
|
|
54
51
|
}
|
|
55
52
|
|
|
56
|
-
|
|
57
53
|
const mergedOptions = { ...this.globalOptions, ...dynamicOptions };
|
|
58
54
|
const customFilter = new FilterXSS(mergedOptions);
|
|
59
|
-
|
|
55
|
+
|
|
60
56
|
return customFilter.process(input);
|
|
61
57
|
|
|
62
58
|
} catch (err: any) {
|
|
63
|
-
logger.error("XSS
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
logger.error("XSS sanitization failed", {
|
|
60
|
+
adapter: "xss",
|
|
61
|
+
operation: "sanitize",
|
|
62
|
+
reason: err?.message
|
|
66
63
|
});
|
|
64
|
+
|
|
67
65
|
throw new AdapterError("XSS sanitizer failed.");
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
|
|
72
69
|
middleware(dynamicOptions?: XSSOptions) {
|
|
73
70
|
return (req: any, _res: any, next: any) => {
|
|
74
71
|
try {
|
|
75
72
|
if (req.body && typeof req.body === "object") {
|
|
76
73
|
const originalBody = req.body;
|
|
77
74
|
const sanitizedBody: any = Array.isArray(originalBody) ? [] : {};
|
|
78
|
-
|
|
75
|
+
|
|
79
76
|
for (const key of Object.keys(originalBody)) {
|
|
80
77
|
const val = originalBody[key];
|
|
81
78
|
|
|
82
79
|
if (typeof val === "string") {
|
|
83
80
|
sanitizedBody[key] = this.sanitize(val, dynamicOptions);
|
|
84
81
|
} else if (Array.isArray(val)) {
|
|
85
|
-
sanitizedBody[key] = val.map(
|
|
82
|
+
sanitizedBody[key] = val.map(v =>
|
|
86
83
|
typeof v === "string"
|
|
87
84
|
? this.sanitize(v, dynamicOptions)
|
|
88
85
|
: v
|
|
89
86
|
);
|
|
90
87
|
} else if (val && typeof val === "object") {
|
|
91
|
-
|
|
92
88
|
sanitizedBody[key] = this.deepSanitize(val, dynamicOptions);
|
|
93
89
|
} else {
|
|
94
90
|
sanitizedBody[key] = val;
|
|
95
91
|
}
|
|
96
92
|
}
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
|
|
99
94
|
req.sanitizedBody = sanitizedBody;
|
|
95
|
+
|
|
100
96
|
|
|
101
|
-
logger.
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
logger.info("XSS sanitization applied", {
|
|
98
|
+
adapter: "xss",
|
|
99
|
+
operation: "middleware",
|
|
100
|
+
keys: Object.keys(sanitizedBody)
|
|
104
101
|
});
|
|
105
102
|
}
|
|
106
103
|
|
|
107
104
|
next();
|
|
108
105
|
} catch (err: any) {
|
|
109
106
|
logger.error("XSS middleware failed", {
|
|
110
|
-
|
|
107
|
+
adapter: "xss",
|
|
108
|
+
operation: "middleware",
|
|
109
|
+
reason: err?.message
|
|
111
110
|
});
|
|
112
111
|
next(err);
|
|
113
112
|
}
|
|
114
113
|
};
|
|
115
114
|
}
|
|
116
115
|
|
|
117
|
-
|
|
118
116
|
private deepSanitize(obj: any, options?: XSSOptions, visited = new WeakSet()): any {
|
|
119
|
-
|
|
120
117
|
if (obj && typeof obj === "object") {
|
|
121
|
-
if (visited.has(obj))
|
|
122
|
-
return obj;
|
|
123
|
-
}
|
|
118
|
+
if (visited.has(obj)) return obj;
|
|
124
119
|
visited.add(obj);
|
|
125
120
|
}
|
|
126
121
|
|
|
@@ -142,4 +137,4 @@ export class XSSAdapter {
|
|
|
142
137
|
|
|
143
138
|
return obj;
|
|
144
139
|
}
|
|
145
|
-
}
|
|
140
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ZodSchema, ZodError } from "zod";
|
|
2
|
-
import { ValidationError } from "../core/errors/ValidationError
|
|
3
|
-
import { logger } from "../logging
|
|
2
|
+
import { ValidationError } from "../core/errors/ValidationError";
|
|
3
|
+
import { logger } from "../logging";
|
|
4
4
|
|
|
5
5
|
export class ZodAdapter {
|
|
6
6
|
private globalSchema?: ZodSchema;
|
|
@@ -10,13 +10,11 @@ export class ZodAdapter {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
validate(dynamicSchema?: ZodSchema) {
|
|
13
|
-
return (req: any,
|
|
13
|
+
return (req: any, _res: any, next: any) => {
|
|
14
14
|
const schema = dynamicSchema || this.globalSchema;
|
|
15
|
-
|
|
16
15
|
if (!schema) return next();
|
|
17
16
|
|
|
18
17
|
const result = schema.safeParse(req.body);
|
|
19
|
-
|
|
20
18
|
if (result.success) return next();
|
|
21
19
|
|
|
22
20
|
const zodErr: ZodError = result.error;
|
|
@@ -28,15 +26,17 @@ export class ZodAdapter {
|
|
|
28
26
|
}));
|
|
29
27
|
|
|
30
28
|
logger.warn("Zod validation failed", {
|
|
31
|
-
|
|
29
|
+
adapter: "zod",
|
|
30
|
+
operation: "validate",
|
|
32
31
|
method: req.method,
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
path: req.path,
|
|
33
|
+
issueCount: issues.length,
|
|
34
|
+
issues
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
return next(
|
|
38
|
-
new ValidationError("Validation failed.", issues as any)
|
|
38
|
+
new ValidationError("Validation failed.", issues as any)
|
|
39
39
|
);
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
}
|
|
42
|
+
}
|