@nest-omni/core 4.1.3-19 → 4.1.3-20
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/cache/cache.module.d.ts +0 -6
- package/cache/cache.module.js +7 -7
- package/cache/cache.service.js +12 -0
- package/cache/dependencies/db.dependency.d.ts +0 -13
- package/cache/dependencies/db.dependency.js +0 -16
- package/cache/dependencies/tag.dependency.d.ts +39 -4
- package/cache/dependencies/tag.dependency.js +109 -11
- package/cache/interfaces/cache-options.interface.d.ts +8 -0
- package/cache/providers/memory-cache.provider.d.ts +20 -0
- package/cache/providers/memory-cache.provider.js +40 -0
- package/http-client/config/http-client.config.d.ts +5 -0
- package/http-client/config/http-client.config.js +24 -13
- package/http-client/decorators/http-client.decorators.d.ts +1 -25
- package/http-client/decorators/http-client.decorators.js +97 -90
- package/http-client/entities/http-log.entity.d.ts +0 -20
- package/http-client/entities/http-log.entity.js +0 -12
- package/http-client/examples/advanced-usage.example.d.ts +4 -5
- package/http-client/examples/advanced-usage.example.js +4 -56
- package/http-client/http-client.module.d.ts +35 -2
- package/http-client/http-client.module.js +80 -75
- package/http-client/index.d.ts +1 -1
- package/http-client/interfaces/api-client-config.interface.d.ts +1 -91
- package/http-client/interfaces/http-client-config.interface.d.ts +53 -62
- package/http-client/services/api-client-registry.service.d.ts +5 -23
- package/http-client/services/api-client-registry.service.js +41 -284
- package/http-client/services/circuit-breaker.service.d.ts +69 -2
- package/http-client/services/circuit-breaker.service.js +185 -7
- package/http-client/services/http-client.service.d.ts +58 -23
- package/http-client/services/http-client.service.js +294 -150
- package/http-client/services/http-log-query.service.js +0 -13
- package/http-client/services/index.d.ts +0 -1
- package/http-client/services/index.js +0 -1
- package/http-client/services/logging.service.d.ts +79 -10
- package/http-client/services/logging.service.js +246 -51
- package/http-client/utils/call-stack-extractor.util.d.ts +26 -0
- package/http-client/utils/call-stack-extractor.util.js +35 -0
- package/http-client/utils/security-validator.util.d.ts +118 -0
- package/http-client/utils/security-validator.util.js +352 -0
- package/package.json +1 -1
- package/redis-lock/lock-heartbeat.service.d.ts +2 -0
- package/redis-lock/lock-heartbeat.service.js +12 -2
- package/redis-lock/redis-lock.service.d.ts +4 -0
- package/redis-lock/redis-lock.service.js +61 -8
- package/http-client/services/cache.service.d.ts +0 -76
- package/http-client/services/cache.service.js +0 -333
|
@@ -6,6 +6,33 @@ exports.CallStackExtractor = void 0;
|
|
|
6
6
|
* 用于自动提取HTTP请求的调用服务和方法信息
|
|
7
7
|
*/
|
|
8
8
|
class CallStackExtractor {
|
|
9
|
+
/**
|
|
10
|
+
* 设置装饰器上下文
|
|
11
|
+
*/
|
|
12
|
+
static setDecoratorContext(context) {
|
|
13
|
+
// 只在最外层设置
|
|
14
|
+
if (this.decoratorContextDepth === 0) {
|
|
15
|
+
this.decoratorContext = context;
|
|
16
|
+
}
|
|
17
|
+
this.decoratorContextDepth++;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 获取装饰器上下文
|
|
21
|
+
*/
|
|
22
|
+
static getDecoratorContext() {
|
|
23
|
+
return this.decoratorContext;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 清除装饰器上下文
|
|
27
|
+
*/
|
|
28
|
+
static clearDecoratorContext() {
|
|
29
|
+
this.decoratorContextDepth--;
|
|
30
|
+
// 只在最外层清除
|
|
31
|
+
if (this.decoratorContextDepth <= 0) {
|
|
32
|
+
this.decoratorContext = null;
|
|
33
|
+
this.decoratorContextDepth = 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
9
36
|
/**
|
|
10
37
|
* 从调用栈中提取服务和方法信息
|
|
11
38
|
*/
|
|
@@ -184,3 +211,11 @@ class CallStackExtractor {
|
|
|
184
211
|
}
|
|
185
212
|
}
|
|
186
213
|
exports.CallStackExtractor = CallStackExtractor;
|
|
214
|
+
/**
|
|
215
|
+
* 存储装饰器上下文(用于传递装饰器信息到HTTP客户端)
|
|
216
|
+
*/
|
|
217
|
+
CallStackExtractor.decoratorContext = null;
|
|
218
|
+
/**
|
|
219
|
+
* 装饰器上下文引用计数,用于处理多个装饰器嵌套的情况
|
|
220
|
+
*/
|
|
221
|
+
CallStackExtractor.decoratorContextDepth = 0;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF防护配置
|
|
3
|
+
*/
|
|
4
|
+
export interface SSRFProtectionConfig {
|
|
5
|
+
/** 是否启用SSRF防护 */
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
/** 允许的URL协议 */
|
|
8
|
+
allowedProtocols: string[];
|
|
9
|
+
/** 禁止访问的IP段(私有网络、本地回环等) */
|
|
10
|
+
blockedIPRanges: string[];
|
|
11
|
+
/** 允许的主机名白名单 */
|
|
12
|
+
allowedHostnames?: string[];
|
|
13
|
+
/** 禁止的主机名黑名单 */
|
|
14
|
+
blockedHostnames?: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* URL验证配置
|
|
18
|
+
*/
|
|
19
|
+
export interface URLValidationConfig {
|
|
20
|
+
/** 最大URL长度 */
|
|
21
|
+
maxURLLength: number;
|
|
22
|
+
/** 是否允许本地回环地址 */
|
|
23
|
+
allowLoopback: boolean;
|
|
24
|
+
/** 是否允许私有网络地址 */
|
|
25
|
+
allowPrivateNetwork: boolean;
|
|
26
|
+
/** 是否允许Link-local地址 */
|
|
27
|
+
allowLinkLocal: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 安全验证工具类
|
|
31
|
+
* 提供URL验证、SSRF防护等功能
|
|
32
|
+
*/
|
|
33
|
+
export declare class SecurityValidator {
|
|
34
|
+
private static readonly logger;
|
|
35
|
+
/**
|
|
36
|
+
* 默认SSRF防护配置
|
|
37
|
+
*/
|
|
38
|
+
private static readonly defaultSSRFConfig;
|
|
39
|
+
/**
|
|
40
|
+
* 默认URL验证配置
|
|
41
|
+
*/
|
|
42
|
+
private static readonly defaultURLConfig;
|
|
43
|
+
/**
|
|
44
|
+
* 验证URL安全性
|
|
45
|
+
* @param url 要验证的URL字符串
|
|
46
|
+
* @param urlConfig URL验证配置
|
|
47
|
+
* @returns 验证结果和错误信息
|
|
48
|
+
*/
|
|
49
|
+
static validateURL(url: string, urlConfig?: Partial<URLValidationConfig>): {
|
|
50
|
+
valid: boolean;
|
|
51
|
+
error?: string;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* SSRF防护检查
|
|
55
|
+
* @param url 要检查的URL
|
|
56
|
+
* @param ssrfConfig SSRF防护配置
|
|
57
|
+
* @returns 检查结果和错误信息
|
|
58
|
+
*/
|
|
59
|
+
static checkSSRF(url: string, ssrfConfig?: Partial<SSRFProtectionConfig>): {
|
|
60
|
+
safe: boolean;
|
|
61
|
+
error?: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* 清理和验证URL
|
|
65
|
+
* 综合验证和SSRF防护
|
|
66
|
+
* @param url 要清理的URL
|
|
67
|
+
* @param config 完整的安全配置
|
|
68
|
+
* @returns 清理后的URL和验证结果
|
|
69
|
+
*/
|
|
70
|
+
static sanitizeURL(url: string, config?: {
|
|
71
|
+
urlConfig?: Partial<URLValidationConfig>;
|
|
72
|
+
ssrfConfig?: Partial<SSRFProtectionConfig>;
|
|
73
|
+
}): {
|
|
74
|
+
url: string;
|
|
75
|
+
valid: boolean;
|
|
76
|
+
error?: string;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* 从主机名中提取IP地址
|
|
80
|
+
*/
|
|
81
|
+
private static extractIPAddress;
|
|
82
|
+
/**
|
|
83
|
+
* 验证IP地址
|
|
84
|
+
*/
|
|
85
|
+
private static validateIPAddress;
|
|
86
|
+
/**
|
|
87
|
+
* 检查IP地址是否在阻止的范围内
|
|
88
|
+
*/
|
|
89
|
+
private static checkIPRange;
|
|
90
|
+
/**
|
|
91
|
+
* 检查IP地址是否在指定范围内
|
|
92
|
+
* 简化版本,主要用于常见CIDR范围
|
|
93
|
+
*/
|
|
94
|
+
private static isIPInRange;
|
|
95
|
+
/**
|
|
96
|
+
* 验证主机名格式
|
|
97
|
+
*/
|
|
98
|
+
private static isValidHostname;
|
|
99
|
+
/**
|
|
100
|
+
* 敏感数据检测
|
|
101
|
+
* 检查数据中是否包含敏感信息(如密码、token等)
|
|
102
|
+
* @param data 要检查的数据对象
|
|
103
|
+
* @param additionalPatterns 额外的敏感数据模式
|
|
104
|
+
* @returns 检测结果
|
|
105
|
+
*/
|
|
106
|
+
static detectSensitiveData(data: any, additionalPatterns?: RegExp[]): {
|
|
107
|
+
hasSensitiveData: boolean;
|
|
108
|
+
fields: string[];
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* 获取默认SSRF防护配置
|
|
112
|
+
*/
|
|
113
|
+
static getDefaultSSRFConfig(): SSRFProtectionConfig;
|
|
114
|
+
/**
|
|
115
|
+
* 获取默认URL验证配置
|
|
116
|
+
*/
|
|
117
|
+
static getDefaultURLConfig(): URLValidationConfig;
|
|
118
|
+
}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SecurityValidator = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
/**
|
|
6
|
+
* 安全验证工具类
|
|
7
|
+
* 提供URL验证、SSRF防护等功能
|
|
8
|
+
*/
|
|
9
|
+
class SecurityValidator {
|
|
10
|
+
/**
|
|
11
|
+
* 验证URL安全性
|
|
12
|
+
* @param url 要验证的URL字符串
|
|
13
|
+
* @param urlConfig URL验证配置
|
|
14
|
+
* @returns 验证结果和错误信息
|
|
15
|
+
*/
|
|
16
|
+
static validateURL(url, urlConfig = {}) {
|
|
17
|
+
const config = Object.assign(Object.assign({}, this.defaultURLConfig), urlConfig);
|
|
18
|
+
// 检查URL长度
|
|
19
|
+
if (url.length > config.maxURLLength) {
|
|
20
|
+
return {
|
|
21
|
+
valid: false,
|
|
22
|
+
error: `URL length exceeds maximum allowed length of ${config.maxURLLength}`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
let parsedURL;
|
|
26
|
+
try {
|
|
27
|
+
parsedURL = new URL(url);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return {
|
|
31
|
+
valid: false,
|
|
32
|
+
error: `Invalid URL format: ${error instanceof Error ? error.message : String(error)}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// 检查协议
|
|
36
|
+
if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsedURL.protocol)) {
|
|
37
|
+
return {
|
|
38
|
+
valid: false,
|
|
39
|
+
error: `Unsupported protocol: ${parsedURL.protocol}`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// 检查主机名
|
|
43
|
+
if (!parsedURL.hostname) {
|
|
44
|
+
return {
|
|
45
|
+
valid: false,
|
|
46
|
+
error: 'Hostname is required',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// 检查是否为IP地址
|
|
50
|
+
const ipAddress = this.extractIPAddress(parsedURL.hostname);
|
|
51
|
+
if (ipAddress) {
|
|
52
|
+
return this.validateIPAddress(ipAddress, config);
|
|
53
|
+
}
|
|
54
|
+
// 检查主机名格式
|
|
55
|
+
if (!this.isValidHostname(parsedURL.hostname)) {
|
|
56
|
+
return {
|
|
57
|
+
valid: false,
|
|
58
|
+
error: `Invalid hostname format: ${parsedURL.hostname}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { valid: true };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* SSRF防护检查
|
|
65
|
+
* @param url 要检查的URL
|
|
66
|
+
* @param ssrfConfig SSRF防护配置
|
|
67
|
+
* @returns 检查结果和错误信息
|
|
68
|
+
*/
|
|
69
|
+
static checkSSRF(url, ssrfConfig = {}) {
|
|
70
|
+
const config = Object.assign(Object.assign({}, this.defaultSSRFConfig), ssrfConfig);
|
|
71
|
+
if (!config.enabled) {
|
|
72
|
+
return { safe: true };
|
|
73
|
+
}
|
|
74
|
+
let parsedURL;
|
|
75
|
+
try {
|
|
76
|
+
parsedURL = new URL(url);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return {
|
|
80
|
+
safe: false,
|
|
81
|
+
error: `Invalid URL format: ${error instanceof Error ? error.message : String(error)}`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// 检查协议
|
|
85
|
+
if (!config.allowedProtocols.includes(parsedURL.protocol)) {
|
|
86
|
+
return {
|
|
87
|
+
safe: false,
|
|
88
|
+
error: `Protocol not allowed: ${parsedURL.protocol}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// 检查黑名单主机名
|
|
92
|
+
if (config.blockedHostnames) {
|
|
93
|
+
if (config.blockedHostnames.includes(parsedURL.hostname)) {
|
|
94
|
+
return {
|
|
95
|
+
safe: false,
|
|
96
|
+
error: `Hostname is blocked: ${parsedURL.hostname}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// 检查白名单主机名(如果配置了)
|
|
101
|
+
if (config.allowedHostnames && config.allowedHostnames.length > 0) {
|
|
102
|
+
if (!config.allowedHostnames.includes(parsedURL.hostname)) {
|
|
103
|
+
return {
|
|
104
|
+
safe: false,
|
|
105
|
+
error: `Hostname not in whitelist: ${parsedURL.hostname}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// 检查IP地址范围
|
|
110
|
+
const ipAddress = this.extractIPAddress(parsedURL.hostname);
|
|
111
|
+
if (ipAddress) {
|
|
112
|
+
const ipCheck = this.checkIPRange(ipAddress, config.blockedIPRanges);
|
|
113
|
+
if (!ipCheck.allowed) {
|
|
114
|
+
return {
|
|
115
|
+
safe: false,
|
|
116
|
+
error: `IP address is blocked: ${ipAddress} (${ipCheck.reason})`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { safe: true };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 清理和验证URL
|
|
124
|
+
* 综合验证和SSRF防护
|
|
125
|
+
* @param url 要清理的URL
|
|
126
|
+
* @param config 完整的安全配置
|
|
127
|
+
* @returns 清理后的URL和验证结果
|
|
128
|
+
*/
|
|
129
|
+
static sanitizeURL(url, config = {}) {
|
|
130
|
+
// 去除首尾空格
|
|
131
|
+
let sanitizedURL = url.trim();
|
|
132
|
+
// 移除可能的换行符和控制字符
|
|
133
|
+
sanitizedURL = sanitizedURL.replace(/[\r\n\t]/g, '');
|
|
134
|
+
// 验证URL格式
|
|
135
|
+
const validationResult = this.validateURL(sanitizedURL, config.urlConfig);
|
|
136
|
+
if (!validationResult.valid) {
|
|
137
|
+
return { url: sanitizedURL, valid: false, error: validationResult.error };
|
|
138
|
+
}
|
|
139
|
+
// SSRF防护检查
|
|
140
|
+
const ssrfResult = this.checkSSRF(sanitizedURL, config.ssrfConfig);
|
|
141
|
+
if (!ssrfResult.safe) {
|
|
142
|
+
return { url: sanitizedURL, valid: false, error: ssrfResult.error };
|
|
143
|
+
}
|
|
144
|
+
return { url: sanitizedURL, valid: true };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 从主机名中提取IP地址
|
|
148
|
+
*/
|
|
149
|
+
static extractIPAddress(hostname) {
|
|
150
|
+
// IPv4地址
|
|
151
|
+
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
152
|
+
const ipv4Match = hostname.match(ipv4Regex);
|
|
153
|
+
if (ipv4Match) {
|
|
154
|
+
return hostname;
|
|
155
|
+
}
|
|
156
|
+
// IPv6地址(简化版)
|
|
157
|
+
if (hostname.includes(':') && !hostname.includes('.')) {
|
|
158
|
+
return hostname;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* 验证IP地址
|
|
164
|
+
*/
|
|
165
|
+
static validateIPAddress(ipAddress, config) {
|
|
166
|
+
// 检查是否为本地回环地址
|
|
167
|
+
if (!config.allowLoopback) {
|
|
168
|
+
if (ipAddress === '127.0.0.1' || ipAddress === '::1' || ipAddress.startsWith('127.')) {
|
|
169
|
+
return {
|
|
170
|
+
valid: false,
|
|
171
|
+
error: 'Loopback addresses are not allowed',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// 检查是否为私有网络地址
|
|
176
|
+
if (!config.allowPrivateNetwork) {
|
|
177
|
+
if (ipAddress.startsWith('10.') ||
|
|
178
|
+
ipAddress.startsWith('172.16.') ||
|
|
179
|
+
ipAddress.startsWith('192.168.') ||
|
|
180
|
+
ipAddress.startsWith('fc00:') ||
|
|
181
|
+
ipAddress.startsWith('fd')) {
|
|
182
|
+
return {
|
|
183
|
+
valid: false,
|
|
184
|
+
error: 'Private network addresses are not allowed',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// 检查是否为Link-local地址
|
|
189
|
+
if (!config.allowLinkLocal) {
|
|
190
|
+
if (ipAddress.startsWith('169.254.') || ipAddress.startsWith('fe80:')) {
|
|
191
|
+
return {
|
|
192
|
+
valid: false,
|
|
193
|
+
error: 'Link-local addresses are not allowed',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { valid: true };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* 检查IP地址是否在阻止的范围内
|
|
201
|
+
*/
|
|
202
|
+
static checkIPRange(ipAddress, blockedRanges) {
|
|
203
|
+
for (const range of blockedRanges) {
|
|
204
|
+
if (this.isIPInRange(ipAddress, range)) {
|
|
205
|
+
return {
|
|
206
|
+
allowed: false,
|
|
207
|
+
reason: `IP address is in blocked range: ${range}`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { allowed: true };
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 检查IP地址是否在指定范围内
|
|
215
|
+
* 简化版本,主要用于常见CIDR范围
|
|
216
|
+
*/
|
|
217
|
+
static isIPInRange(ipAddress, cidrRange) {
|
|
218
|
+
// 处理IPv4 CIDR
|
|
219
|
+
const [range, prefixStr] = cidrRange.split('/');
|
|
220
|
+
const prefix = parseInt(prefixStr, 10);
|
|
221
|
+
// 精确匹配
|
|
222
|
+
if (ipAddress === range) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
// 简化的CIDR匹配(仅支持常见范围)
|
|
226
|
+
if (cidrRange.includes('/')) {
|
|
227
|
+
// IPv4范围检查
|
|
228
|
+
if (range.includes('.') && ipAddress.includes('.')) {
|
|
229
|
+
const rangeParts = range.split('.').map(Number);
|
|
230
|
+
const ipParts = ipAddress.split('.').map(Number);
|
|
231
|
+
if (prefix === 8) {
|
|
232
|
+
return ipParts[0] === rangeParts[0];
|
|
233
|
+
}
|
|
234
|
+
else if (prefix === 16) {
|
|
235
|
+
return ipParts[0] === rangeParts[0] && ipParts[1] === rangeParts[1];
|
|
236
|
+
}
|
|
237
|
+
else if (prefix === 24) {
|
|
238
|
+
return (ipParts[0] === rangeParts[0] &&
|
|
239
|
+
ipParts[1] === rangeParts[1] &&
|
|
240
|
+
ipParts[2] === rangeParts[2]);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* 验证主机名格式
|
|
248
|
+
*/
|
|
249
|
+
static isValidHostname(hostname) {
|
|
250
|
+
// RFC 1123主机名规范
|
|
251
|
+
const hostnameRegex = /^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$/;
|
|
252
|
+
return hostnameRegex.test(hostname);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* 敏感数据检测
|
|
256
|
+
* 检查数据中是否包含敏感信息(如密码、token等)
|
|
257
|
+
* @param data 要检查的数据对象
|
|
258
|
+
* @param additionalPatterns 额外的敏感数据模式
|
|
259
|
+
* @returns 检测结果
|
|
260
|
+
*/
|
|
261
|
+
static detectSensitiveData(data, additionalPatterns = []) {
|
|
262
|
+
const sensitiveFields = [];
|
|
263
|
+
// 默认敏感字段名模式
|
|
264
|
+
const defaultPatterns = [
|
|
265
|
+
/password/i,
|
|
266
|
+
/secret/i,
|
|
267
|
+
/token/i,
|
|
268
|
+
/api[_-]?key/i,
|
|
269
|
+
/authorization/i,
|
|
270
|
+
/credential/i,
|
|
271
|
+
/private[_-]?key/i,
|
|
272
|
+
/access[_-]?token/i,
|
|
273
|
+
/refresh[_-]?token/i,
|
|
274
|
+
/session[_-]?id/i,
|
|
275
|
+
/csrf/i,
|
|
276
|
+
/ssn/i,
|
|
277
|
+
/credit[_-]?card/i,
|
|
278
|
+
];
|
|
279
|
+
const allPatterns = [...defaultPatterns, ...additionalPatterns];
|
|
280
|
+
const checkObject = (obj, path = '') => {
|
|
281
|
+
if (!obj || typeof obj !== 'object') {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
for (const key in obj) {
|
|
285
|
+
if (!obj.hasOwnProperty(key)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
289
|
+
// 检查键名是否匹配敏感模式
|
|
290
|
+
if (allPatterns.some((pattern) => pattern.test(key))) {
|
|
291
|
+
sensitiveFields.push(currentPath);
|
|
292
|
+
}
|
|
293
|
+
// 递归检查嵌套对象
|
|
294
|
+
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
295
|
+
checkObject(obj[key], currentPath);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
checkObject(data);
|
|
300
|
+
return {
|
|
301
|
+
hasSensitiveData: sensitiveFields.length > 0,
|
|
302
|
+
fields: sensitiveFields,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* 获取默认SSRF防护配置
|
|
307
|
+
*/
|
|
308
|
+
static getDefaultSSRFConfig() {
|
|
309
|
+
return Object.assign({}, this.defaultSSRFConfig);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 获取默认URL验证配置
|
|
313
|
+
*/
|
|
314
|
+
static getDefaultURLConfig() {
|
|
315
|
+
return Object.assign({}, this.defaultURLConfig);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
exports.SecurityValidator = SecurityValidator;
|
|
319
|
+
SecurityValidator.logger = new common_1.Logger(SecurityValidator.name);
|
|
320
|
+
/**
|
|
321
|
+
* 默认SSRF防护配置
|
|
322
|
+
*/
|
|
323
|
+
SecurityValidator.defaultSSRFConfig = {
|
|
324
|
+
enabled: true,
|
|
325
|
+
allowedProtocols: ['http:', 'https:'],
|
|
326
|
+
blockedIPRanges: [
|
|
327
|
+
'127.0.0.0/8', // Loopback
|
|
328
|
+
'10.0.0.0/8', // Private Class A
|
|
329
|
+
'172.16.0.0/12', // Private Class B
|
|
330
|
+
'192.168.0.0/16', // Private Class C
|
|
331
|
+
'169.254.0.0/16', // Link-local
|
|
332
|
+
'::1/128', // IPv6 loopback
|
|
333
|
+
'fc00::/7', // IPv6 private
|
|
334
|
+
'fe80::/10', // IPv6 link-local
|
|
335
|
+
'0.0.0.0/8', // Current network
|
|
336
|
+
],
|
|
337
|
+
allowedHostnames: undefined,
|
|
338
|
+
blockedHostnames: [
|
|
339
|
+
'localhost',
|
|
340
|
+
'metadata.google.internal', // GCP metadata
|
|
341
|
+
'169.254.169.254', // AWS/GCP/Azure metadata
|
|
342
|
+
],
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* 默认URL验证配置
|
|
346
|
+
*/
|
|
347
|
+
SecurityValidator.defaultURLConfig = {
|
|
348
|
+
maxURLLength: 2000,
|
|
349
|
+
allowLoopback: false,
|
|
350
|
+
allowPrivateNetwork: false,
|
|
351
|
+
allowLinkLocal: false,
|
|
352
|
+
};
|
package/package.json
CHANGED
|
@@ -68,6 +68,8 @@ export declare class LockHeartbeatService implements OnModuleInit, OnModuleDestr
|
|
|
68
68
|
/**
|
|
69
69
|
* Get all active instances (with heartbeat)
|
|
70
70
|
*
|
|
71
|
+
* **Note**: Uses SCAN command to avoid blocking Redis in production.
|
|
72
|
+
*
|
|
71
73
|
* @returns Array of instance IDs that are currently alive
|
|
72
74
|
*/
|
|
73
75
|
getActiveInstances(): Promise<string[]>;
|
|
@@ -182,6 +182,8 @@ let LockHeartbeatService = LockHeartbeatService_1 = class LockHeartbeatService {
|
|
|
182
182
|
/**
|
|
183
183
|
* Get all active instances (with heartbeat)
|
|
184
184
|
*
|
|
185
|
+
* **Note**: Uses SCAN command to avoid blocking Redis in production.
|
|
186
|
+
*
|
|
185
187
|
* @returns Array of instance IDs that are currently alive
|
|
186
188
|
*/
|
|
187
189
|
getActiveInstances() {
|
|
@@ -190,8 +192,16 @@ let LockHeartbeatService = LockHeartbeatService_1 = class LockHeartbeatService {
|
|
|
190
192
|
return [];
|
|
191
193
|
}
|
|
192
194
|
try {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
+
let cursor = '0';
|
|
196
|
+
const pattern = 'heartbeat:*';
|
|
197
|
+
const allKeys = [];
|
|
198
|
+
// Use SCAN to iterate through keys without blocking Redis
|
|
199
|
+
do {
|
|
200
|
+
const [nextCursor, keys] = yield this.redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
|
|
201
|
+
cursor = nextCursor;
|
|
202
|
+
allKeys.push(...keys);
|
|
203
|
+
} while (cursor !== '0');
|
|
204
|
+
return allKeys.map((key) => key.replace('heartbeat:', ''));
|
|
195
205
|
}
|
|
196
206
|
catch (error) {
|
|
197
207
|
this.logger.error('Error getting active instances:', error);
|
|
@@ -247,6 +247,8 @@ export declare class RedisLockService implements OnModuleInit {
|
|
|
247
247
|
*
|
|
248
248
|
* WARNING: Use with caution in production! This will forcefully delete locks.
|
|
249
249
|
*
|
|
250
|
+
* **Note**: Uses SCAN command to avoid blocking Redis in production.
|
|
251
|
+
*
|
|
250
252
|
* @param pattern - Lock pattern (e.g., 'MyService:*' or '*:migration:*')
|
|
251
253
|
* @param keyPrefix - Key prefix (default: 'lock')
|
|
252
254
|
* @returns Number of locks deleted
|
|
@@ -299,6 +301,8 @@ export declare class RedisLockService implements OnModuleInit {
|
|
|
299
301
|
* Get all active locks with optional pattern filtering
|
|
300
302
|
* Useful for monitoring and debugging
|
|
301
303
|
*
|
|
304
|
+
* **Note**: Uses SCAN command to avoid blocking Redis in production.
|
|
305
|
+
*
|
|
302
306
|
* @param pattern - Optional pattern to filter locks (e.g., 'MyService:*')
|
|
303
307
|
* @param keyPrefix - Key prefix (default: 'lock')
|
|
304
308
|
* @returns Array of active lock information
|
|
@@ -175,6 +175,34 @@ let RedisLockService = RedisLockService_1 = class RedisLockService {
|
|
|
175
175
|
var _a, _b, _c, _d, _e;
|
|
176
176
|
const redis = yield this.getRedis();
|
|
177
177
|
const opts = Object.assign(Object.assign({}, this.defaultOptions), options);
|
|
178
|
+
// Validate TTL
|
|
179
|
+
if (opts.ttl !== undefined) {
|
|
180
|
+
if (typeof opts.ttl !== 'number' || opts.ttl <= 0) {
|
|
181
|
+
this.logger.warn(`Invalid TTL value: ${opts.ttl}. TTL must be a positive number. Using default TTL: ${this.defaultOptions.ttl}ms`);
|
|
182
|
+
opts.ttl = this.defaultOptions.ttl;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Validate waitTimeout
|
|
186
|
+
if (opts.waitTimeout !== undefined) {
|
|
187
|
+
if (typeof opts.waitTimeout !== 'number' || opts.waitTimeout <= 0) {
|
|
188
|
+
this.logger.warn(`Invalid waitTimeout value: ${opts.waitTimeout}. waitTimeout must be a positive number. Ignoring waitTimeout.`);
|
|
189
|
+
opts.waitTimeout = undefined;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Validate retryCount
|
|
193
|
+
if (opts.retryCount !== undefined) {
|
|
194
|
+
if (typeof opts.retryCount !== 'number' || opts.retryCount < 0) {
|
|
195
|
+
this.logger.warn(`Invalid retryCount value: ${opts.retryCount}. retryCount must be a non-negative number. Using default: ${this.defaultOptions.retryCount}`);
|
|
196
|
+
opts.retryCount = this.defaultOptions.retryCount;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Validate retryDelay
|
|
200
|
+
if (opts.retryDelay !== undefined) {
|
|
201
|
+
if (typeof opts.retryDelay !== 'number' || opts.retryDelay <= 0) {
|
|
202
|
+
this.logger.warn(`Invalid retryDelay value: ${opts.retryDelay}. retryDelay must be a positive number. Using default: ${this.defaultOptions.retryDelay}ms`);
|
|
203
|
+
opts.retryDelay = this.defaultOptions.retryDelay;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
178
206
|
// Handle backward compatibility: if throwOnFailure is true, use THROW strategy
|
|
179
207
|
if (opts.throwOnFailure && !options.strategy) {
|
|
180
208
|
opts.strategy = LockStrategy.THROW;
|
|
@@ -471,6 +499,8 @@ let RedisLockService = RedisLockService_1 = class RedisLockService {
|
|
|
471
499
|
*
|
|
472
500
|
* WARNING: Use with caution in production! This will forcefully delete locks.
|
|
473
501
|
*
|
|
502
|
+
* **Note**: Uses SCAN command to avoid blocking Redis in production.
|
|
503
|
+
*
|
|
474
504
|
* @param pattern - Lock pattern (e.g., 'MyService:*' or '*:migration:*')
|
|
475
505
|
* @param keyPrefix - Key prefix (default: 'lock')
|
|
476
506
|
* @returns Number of locks deleted
|
|
@@ -489,14 +519,28 @@ let RedisLockService = RedisLockService_1 = class RedisLockService {
|
|
|
489
519
|
const redis = yield this.getRedis();
|
|
490
520
|
const searchPattern = `${keyPrefix}:${pattern}`;
|
|
491
521
|
try {
|
|
492
|
-
|
|
493
|
-
|
|
522
|
+
let cursor = '0';
|
|
523
|
+
const allKeys = [];
|
|
524
|
+
// Use SCAN to iterate through keys without blocking Redis
|
|
525
|
+
do {
|
|
526
|
+
const [nextCursor, keys] = yield redis.scan(cursor, 'MATCH', searchPattern, 'COUNT', 100);
|
|
527
|
+
cursor = nextCursor;
|
|
528
|
+
allKeys.push(...keys);
|
|
529
|
+
} while (cursor !== '0');
|
|
530
|
+
if (allKeys.length === 0) {
|
|
494
531
|
this.logger.debug(`No locks found matching pattern: ${searchPattern}`);
|
|
495
532
|
return 0;
|
|
496
533
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
534
|
+
// Delete keys in batches to avoid blocking Redis
|
|
535
|
+
let deletedCount = 0;
|
|
536
|
+
const batchSize = 100;
|
|
537
|
+
for (let i = 0; i < allKeys.length; i += batchSize) {
|
|
538
|
+
const batch = allKeys.slice(i, i + batchSize);
|
|
539
|
+
const result = yield redis.del(...batch);
|
|
540
|
+
deletedCount += result;
|
|
541
|
+
}
|
|
542
|
+
this.logger.log(`Cleaned up ${deletedCount} lock(s) matching pattern: ${searchPattern}`);
|
|
543
|
+
return deletedCount;
|
|
500
544
|
}
|
|
501
545
|
catch (error) {
|
|
502
546
|
this.logger.error(`Error cleaning up locks by pattern ${searchPattern}: ${error.message}`, error.stack);
|
|
@@ -578,6 +622,8 @@ let RedisLockService = RedisLockService_1 = class RedisLockService {
|
|
|
578
622
|
* Get all active locks with optional pattern filtering
|
|
579
623
|
* Useful for monitoring and debugging
|
|
580
624
|
*
|
|
625
|
+
* **Note**: Uses SCAN command to avoid blocking Redis in production.
|
|
626
|
+
*
|
|
581
627
|
* @param pattern - Optional pattern to filter locks (e.g., 'MyService:*')
|
|
582
628
|
* @param keyPrefix - Key prefix (default: 'lock')
|
|
583
629
|
* @returns Array of active lock information
|
|
@@ -587,11 +633,18 @@ let RedisLockService = RedisLockService_1 = class RedisLockService {
|
|
|
587
633
|
const redis = yield this.getRedis();
|
|
588
634
|
const searchPattern = `${keyPrefix}:${pattern}`;
|
|
589
635
|
try {
|
|
590
|
-
|
|
591
|
-
|
|
636
|
+
let cursor = '0';
|
|
637
|
+
const allKeys = [];
|
|
638
|
+
// Use SCAN to iterate through keys without blocking Redis
|
|
639
|
+
do {
|
|
640
|
+
const [nextCursor, keys] = yield redis.scan(cursor, 'MATCH', searchPattern, 'COUNT', 100);
|
|
641
|
+
cursor = nextCursor;
|
|
642
|
+
allKeys.push(...keys);
|
|
643
|
+
} while (cursor !== '0');
|
|
644
|
+
if (allKeys.length === 0) {
|
|
592
645
|
return [];
|
|
593
646
|
}
|
|
594
|
-
const locks = yield Promise.all(
|
|
647
|
+
const locks = yield Promise.all(allKeys.map((key) => __awaiter(this, void 0, void 0, function* () {
|
|
595
648
|
const [value, ttl] = yield Promise.all([
|
|
596
649
|
redis.get(key),
|
|
597
650
|
redis.pttl(key),
|