secure-api-guard 0.0.0-stage → 1.0.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/LICENSE +22 -0
- package/README.md +549 -2
- package/package.json +64 -4
- package/src/config/defaults.js +98 -0
- package/src/config/validateConfig.js +148 -0
- package/src/index.js +132 -0
- package/src/middleware/apiKeyAuth.js +210 -0
- package/src/middleware/ipFilter.js +77 -0
- package/src/middleware/rateLimiter.js +129 -0
- package/src/middleware/requestSize.js +157 -0
- package/src/middleware/requestValidator.js +168 -0
- package/src/middleware/securityHeaders.js +59 -0
- package/src/middleware/threatDetector.js +132 -0
- package/src/security/apiKey.js +94 -0
- package/src/security/detector.js +161 -0
- package/src/security/ipAddress.js +224 -0
- package/src/stores/memoryStore.js +135 -0
- package/src/stores/redisStore.js +222 -0
- package/src/types/index.d.ts +313 -0
- package/src/utils/errors.js +88 -0
- package/src/utils/logger.js +229 -0
- package/src/utils/response.js +58 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { defaultLogger } = require('../utils/logger');
|
|
4
|
+
|
|
5
|
+
const LUA_INCR_SCRIPT = `
|
|
6
|
+
local key = KEYS[1]
|
|
7
|
+
local windowMs = tonumber(ARGV[1])
|
|
8
|
+
local current = redis.call('INCR', key)
|
|
9
|
+
if current == 1 then
|
|
10
|
+
redis.call('PEXPIRE', key, windowMs)
|
|
11
|
+
end
|
|
12
|
+
local ttl = redis.call('PTTL', key)
|
|
13
|
+
if ttl < 0 then
|
|
14
|
+
redis.call('PEXPIRE', key, windowMs)
|
|
15
|
+
ttl = windowMs
|
|
16
|
+
end
|
|
17
|
+
return { current, ttl }
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Distributed Redis store for rate limiting across multiple instances.
|
|
22
|
+
*/
|
|
23
|
+
class RedisStore {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} options
|
|
26
|
+
* @param {any} options.client - Redis client (ioredis or node-redis v4+).
|
|
27
|
+
* @param {string} [options.prefix='sag:rl:'] - Key prefix for rate limiting.
|
|
28
|
+
* @param {number} [options.windowMs=60000] - Rate limit window in milliseconds.
|
|
29
|
+
* @param {boolean} [options.passOnConnectionError=true] - If Redis fails, fail open to avoid taking down API.
|
|
30
|
+
* @param {import('../utils/logger').SecurityLogger} [options.logger] - Logger instance.
|
|
31
|
+
*/
|
|
32
|
+
constructor(options = {}) {
|
|
33
|
+
if (!options.client) {
|
|
34
|
+
throw new Error('RedisStore requires a "client" instance (ioredis or redis).');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.client = options.client;
|
|
38
|
+
this.prefix = options.prefix || 'sag:rl:';
|
|
39
|
+
this.windowMs = options.windowMs || 60000;
|
|
40
|
+
this.passOnConnectionError = options.passOnConnectionError !== false;
|
|
41
|
+
this.logger = options.logger || defaultLogger;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Called by express-rate-limit with middleware options.
|
|
46
|
+
* @param {Object} options
|
|
47
|
+
*/
|
|
48
|
+
init(options) {
|
|
49
|
+
if (options && options.windowMs) {
|
|
50
|
+
this.windowMs = options.windowMs;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Prefix a rate limit key.
|
|
56
|
+
* @param {string} key
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
_prefixKey(key) {
|
|
60
|
+
return `${this.prefix}${key}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Execute Lua script across ioredis or node-redis v4+.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} key
|
|
67
|
+
* @param {number} windowMs
|
|
68
|
+
* @returns {Promise<[number, number]>} [currentHits, ttlMs]
|
|
69
|
+
*/
|
|
70
|
+
async _evalLua(key, windowMs) {
|
|
71
|
+
const fullKey = this._prefixKey(key);
|
|
72
|
+
|
|
73
|
+
// Node-redis v4+ client.eval syntax
|
|
74
|
+
if (typeof this.client.eval === 'function') {
|
|
75
|
+
try {
|
|
76
|
+
// Attempt node-redis v4 object parameters
|
|
77
|
+
const res = await this.client.eval(LUA_INCR_SCRIPT, {
|
|
78
|
+
keys: [fullKey],
|
|
79
|
+
arguments: [String(windowMs)]
|
|
80
|
+
});
|
|
81
|
+
return res;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
// If node-redis object parameter syntax failed, try ioredis positional syntax:
|
|
84
|
+
// eval(script, numkeys, key1, ..., arg1, ...)
|
|
85
|
+
const res = await this.client.eval(LUA_INCR_SCRIPT, 1, fullKey, String(windowMs));
|
|
86
|
+
return res;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw new Error('Provided Redis client does not support eval command.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Increment hits for a key.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} key
|
|
97
|
+
* @returns {Promise<{ totalHits: number, resetTime: Date }>}
|
|
98
|
+
*/
|
|
99
|
+
async increment(key) {
|
|
100
|
+
try {
|
|
101
|
+
const result = await this._evalLua(key, this.windowMs);
|
|
102
|
+
const totalHits = Number(result[0]);
|
|
103
|
+
const ttlMs = Number(result[1]);
|
|
104
|
+
const resetTime = new Date(Date.now() + Math.max(0, ttlMs));
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
totalHits,
|
|
108
|
+
resetTime
|
|
109
|
+
};
|
|
110
|
+
} catch (err) {
|
|
111
|
+
this.logger.error('REDIS_STORE_ERROR', `Redis rate-limiter store failure: ${err.message}`, {
|
|
112
|
+
details: { key, error: err.message }
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!this.passOnConnectionError) {
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Fail open: return non-blocking hit count
|
|
120
|
+
return {
|
|
121
|
+
totalHits: 1,
|
|
122
|
+
resetTime: new Date(Date.now() + this.windowMs)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Decrement hits for a key.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} key
|
|
131
|
+
* @returns {Promise<void>}
|
|
132
|
+
*/
|
|
133
|
+
async decrement(key) {
|
|
134
|
+
try {
|
|
135
|
+
const fullKey = this._prefixKey(key);
|
|
136
|
+
if (typeof this.client.decr === 'function') {
|
|
137
|
+
await this.client.decr(fullKey);
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
this.logger.warn('REDIS_STORE_ERROR', `Redis decrement failed: ${err.message}`, {
|
|
141
|
+
details: { key }
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Reset hits for a single key.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} key
|
|
150
|
+
* @returns {Promise<void>}
|
|
151
|
+
*/
|
|
152
|
+
async resetKey(key) {
|
|
153
|
+
try {
|
|
154
|
+
const fullKey = this._prefixKey(key);
|
|
155
|
+
if (typeof this.client.del === 'function') {
|
|
156
|
+
await this.client.del(fullKey);
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
this.logger.warn('REDIS_STORE_ERROR', `Redis resetKey failed: ${err.message}`, {
|
|
160
|
+
details: { key }
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Reset all keys matching prefix.
|
|
167
|
+
*
|
|
168
|
+
* @returns {Promise<void>}
|
|
169
|
+
*/
|
|
170
|
+
async resetAll() {
|
|
171
|
+
try {
|
|
172
|
+
const pattern = `${this.prefix}*`;
|
|
173
|
+
let keys = [];
|
|
174
|
+
|
|
175
|
+
if (typeof this.client.keys === 'function') {
|
|
176
|
+
keys = await this.client.keys(pattern);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (keys && keys.length > 0 && typeof this.client.del === 'function') {
|
|
180
|
+
await this.client.del(...keys);
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
this.logger.warn('REDIS_STORE_ERROR', `Redis resetAll failed: ${err.message}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Retrieve current hits for a key without incrementing.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} key
|
|
191
|
+
* @returns {Promise<{ totalHits: number, resetTime: Date } | undefined>}
|
|
192
|
+
*/
|
|
193
|
+
async get(key) {
|
|
194
|
+
try {
|
|
195
|
+
const fullKey = this._prefixKey(key);
|
|
196
|
+
const [hits, ttl] = await Promise.all([
|
|
197
|
+
this.client.get ? this.client.get(fullKey) : null,
|
|
198
|
+
this.client.pttl ? this.client.pttl(fullKey) : null
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
if (!hits) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const totalHits = parseInt(hits, 10);
|
|
206
|
+
const ttlMs = parseInt(ttl, 10) || this.windowMs;
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
totalHits,
|
|
210
|
+
resetTime: new Date(Date.now() + Math.max(0, ttlMs))
|
|
211
|
+
};
|
|
212
|
+
} catch (err) {
|
|
213
|
+
this.logger.warn('REDIS_STORE_ERROR', `Redis get failed: ${err.message}`);
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
RedisStore
|
|
221
|
+
};
|
|
222
|
+
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
namespace Express {
|
|
5
|
+
interface Request {
|
|
6
|
+
apiKey?: {
|
|
7
|
+
id: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
permissions: string[];
|
|
10
|
+
metadata: Record<string, any>;
|
|
11
|
+
};
|
|
12
|
+
clientIp?: string;
|
|
13
|
+
validated?: {
|
|
14
|
+
body?: any;
|
|
15
|
+
query?: any;
|
|
16
|
+
params?: any;
|
|
17
|
+
headers?: any;
|
|
18
|
+
};
|
|
19
|
+
detectedThreats?: Array<{
|
|
20
|
+
ruleId: string;
|
|
21
|
+
ruleName: string;
|
|
22
|
+
path: string;
|
|
23
|
+
snippet: string;
|
|
24
|
+
severity: string;
|
|
25
|
+
}>;
|
|
26
|
+
id?: string;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SecurityErrorPayload {
|
|
32
|
+
success: false;
|
|
33
|
+
error: {
|
|
34
|
+
code: string;
|
|
35
|
+
message: string;
|
|
36
|
+
details?: any;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class SecurityError extends Error {
|
|
41
|
+
statusCode: number;
|
|
42
|
+
code: string;
|
|
43
|
+
details: any;
|
|
44
|
+
constructor(message: string, statusCode?: number, code?: string, details?: any);
|
|
45
|
+
toJSON(): SecurityErrorPayload;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class RateLimitError extends SecurityError {
|
|
49
|
+
constructor(message?: string, details?: any);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class AuthenticationError extends SecurityError {
|
|
53
|
+
constructor(message?: string, details?: any);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class ForbiddenError extends SecurityError {
|
|
57
|
+
constructor(message?: string, details?: any);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class PayloadTooLargeError extends SecurityError {
|
|
61
|
+
constructor(message?: string, details?: any);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class ValidationError extends SecurityError {
|
|
65
|
+
constructor(message?: string, details?: any);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class ThreatDetectedError extends SecurityError {
|
|
69
|
+
constructor(message?: string, statusCode?: number, details?: any);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class ConfigurationError extends SecurityError {
|
|
73
|
+
constructor(message: string, details?: any);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Stores
|
|
77
|
+
export interface RateLimitStoreRecord {
|
|
78
|
+
totalHits: number;
|
|
79
|
+
resetTime: Date;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class MemoryStore {
|
|
83
|
+
constructor(options?: { windowMs?: number; cleanupIntervalMs?: number });
|
|
84
|
+
init(options: { windowMs?: number }): void;
|
|
85
|
+
increment(key: string): Promise<RateLimitStoreRecord>;
|
|
86
|
+
decrement(key: string): Promise<void>;
|
|
87
|
+
resetKey(key: string): Promise<void>;
|
|
88
|
+
resetAll(): Promise<void>;
|
|
89
|
+
get(key: string): Promise<RateLimitStoreRecord | undefined>;
|
|
90
|
+
shutdown(): void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class RedisStore {
|
|
94
|
+
constructor(options: {
|
|
95
|
+
client: any;
|
|
96
|
+
prefix?: string;
|
|
97
|
+
windowMs?: number;
|
|
98
|
+
passOnConnectionError?: boolean;
|
|
99
|
+
logger?: SecurityLogger;
|
|
100
|
+
});
|
|
101
|
+
init(options: { windowMs?: number }): void;
|
|
102
|
+
increment(key: string): Promise<RateLimitStoreRecord>;
|
|
103
|
+
decrement(key: string): Promise<void>;
|
|
104
|
+
resetKey(key: string): Promise<void>;
|
|
105
|
+
resetAll(): Promise<void>;
|
|
106
|
+
get(key: string): Promise<RateLimitStoreRecord | undefined>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Logger
|
|
110
|
+
export interface SecurityLogEvent {
|
|
111
|
+
timestamp: string;
|
|
112
|
+
level: string;
|
|
113
|
+
eventType: string;
|
|
114
|
+
message: string;
|
|
115
|
+
requestId: string;
|
|
116
|
+
ip: string;
|
|
117
|
+
method?: string;
|
|
118
|
+
path?: string;
|
|
119
|
+
details?: Record<string, any>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class SecurityLogger {
|
|
123
|
+
constructor(options?: {
|
|
124
|
+
enabled?: boolean;
|
|
125
|
+
level?: 'debug' | 'info' | 'warn' | 'error' | 'silent';
|
|
126
|
+
customLogger?: (event: SecurityLogEvent) => void;
|
|
127
|
+
redactFields?: string[];
|
|
128
|
+
});
|
|
129
|
+
log(level: 'debug' | 'info' | 'warn' | 'error', eventType: string, message: string, context?: any): void;
|
|
130
|
+
debug(eventType: string, message: string, context?: any): void;
|
|
131
|
+
info(eventType: string, message: string, context?: any): void;
|
|
132
|
+
warn(eventType: string, message: string, context?: any): void;
|
|
133
|
+
error(eventType: string, message: string, context?: any): void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const defaultLogger: SecurityLogger;
|
|
137
|
+
export function redact(value: any, customSensitiveKeys?: string[]): any;
|
|
138
|
+
export function getRequestId(req: Request): string;
|
|
139
|
+
export const DEFAULT_SENSITIVE_KEYS: string[];
|
|
140
|
+
|
|
141
|
+
// Rate Limiter
|
|
142
|
+
export interface RateLimiterOptions {
|
|
143
|
+
windowMs?: number;
|
|
144
|
+
maxRequests?: number;
|
|
145
|
+
max?: number;
|
|
146
|
+
statusCode?: number;
|
|
147
|
+
message?: string;
|
|
148
|
+
keyGenerator?: (req: Request) => string;
|
|
149
|
+
skip?: (req: Request) => boolean;
|
|
150
|
+
store?: 'memory' | 'redis' | any;
|
|
151
|
+
redisClient?: any;
|
|
152
|
+
redisPrefix?: string;
|
|
153
|
+
passOnRedisError?: boolean;
|
|
154
|
+
handler?: (req: Request, res: Response, error: RateLimitError) => void;
|
|
155
|
+
logger?: SecurityLogger;
|
|
156
|
+
standardHeaders?: boolean | 'draft-6' | 'draft-7';
|
|
157
|
+
legacyHeaders?: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function rateLimiter(options?: RateLimiterOptions): RequestHandler;
|
|
161
|
+
export function createRateLimiter(options?: RateLimiterOptions): RequestHandler;
|
|
162
|
+
export function defaultKeyGenerator(req: Request): string;
|
|
163
|
+
|
|
164
|
+
// API Key Auth
|
|
165
|
+
export interface ApiKeyRecord {
|
|
166
|
+
id?: string;
|
|
167
|
+
name?: string;
|
|
168
|
+
key?: string;
|
|
169
|
+
hash?: string;
|
|
170
|
+
expiresAt?: string | Date | number | null;
|
|
171
|
+
permissions?: string[];
|
|
172
|
+
metadata?: Record<string, any>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface ApiKeyAuthOptions {
|
|
176
|
+
headerName?: string;
|
|
177
|
+
queryParam?: string | null;
|
|
178
|
+
allowBearer?: boolean;
|
|
179
|
+
hashAlgorithm?: string;
|
|
180
|
+
keys?: Array<string | ApiKeyRecord>;
|
|
181
|
+
getKey?: (hash: string, req: Request) => Promise<ApiKeyRecord | null | undefined>;
|
|
182
|
+
verifyKey?: (rawKey: string, req: Request) => Promise<boolean | ApiKeyRecord>;
|
|
183
|
+
requiredPermissions?: string[];
|
|
184
|
+
statusCode?: number;
|
|
185
|
+
forbiddenStatusCode?: number;
|
|
186
|
+
message?: string;
|
|
187
|
+
forbiddenMessage?: string;
|
|
188
|
+
handler?: (req: Request, res: Response, error: SecurityError) => void;
|
|
189
|
+
logger?: SecurityLogger;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function apiKeyAuth(options?: ApiKeyAuthOptions): RequestHandler;
|
|
193
|
+
export function createApiKeyAuth(options?: ApiKeyAuthOptions): RequestHandler;
|
|
194
|
+
export function extractApiKey(req: Request, options: ApiKeyAuthOptions): string | null;
|
|
195
|
+
export function generateApiKey(options?: { prefix?: string; bytes?: number; algorithm?: string }): { key: string; hash: string; prefix: string };
|
|
196
|
+
export function hashApiKey(rawKey: string, algorithm?: string): string;
|
|
197
|
+
export function secureCompare(a: string, b: string): boolean;
|
|
198
|
+
export function isKeyExpired(keyRecord: any): boolean;
|
|
199
|
+
|
|
200
|
+
// IP Filter
|
|
201
|
+
export interface IpFilterOptions {
|
|
202
|
+
mode?: 'allowlist' | 'blocklist';
|
|
203
|
+
allowlist?: string[];
|
|
204
|
+
blocklist?: string[];
|
|
205
|
+
trustProxy?: boolean | string[] | number;
|
|
206
|
+
getIp?: (req: Request) => string;
|
|
207
|
+
statusCode?: number;
|
|
208
|
+
message?: string;
|
|
209
|
+
handler?: (req: Request, res: Response, error: ForbiddenError) => void;
|
|
210
|
+
logger?: SecurityLogger;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function ipFilter(options?: IpFilterOptions): RequestHandler;
|
|
214
|
+
export function createIpFilter(options?: IpFilterOptions): RequestHandler;
|
|
215
|
+
export function parseIp(ipString: string): any;
|
|
216
|
+
export function parseRule(ruleString: string): any;
|
|
217
|
+
export function matchesRule(clientIp: any, rule: any): boolean;
|
|
218
|
+
export function isIpInRules(clientIpStr: string, compiledRules: any[]): boolean;
|
|
219
|
+
export function compileRules(ruleStrings: string[]): any[];
|
|
220
|
+
export function getClientIp(req: Request, options?: any): string;
|
|
221
|
+
|
|
222
|
+
// Request Size
|
|
223
|
+
export interface RequestSizeOptions {
|
|
224
|
+
maxSize?: string | number;
|
|
225
|
+
statusCode?: number;
|
|
226
|
+
message?: string;
|
|
227
|
+
handler?: (req: Request, res: Response, error: PayloadTooLargeError) => void;
|
|
228
|
+
logger?: SecurityLogger;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function requestSize(options?: RequestSizeOptions): RequestHandler;
|
|
232
|
+
export function createRequestSize(options?: RequestSizeOptions): RequestHandler;
|
|
233
|
+
export function parseContentLength(req: Request): { length: number | null; error: string | null };
|
|
234
|
+
|
|
235
|
+
// Security Headers
|
|
236
|
+
export interface SecurityHeadersOptions {
|
|
237
|
+
contentSecurityPolicy?: boolean | any;
|
|
238
|
+
hsts?: boolean | any;
|
|
239
|
+
frameguard?: boolean | any;
|
|
240
|
+
noSniff?: boolean;
|
|
241
|
+
referrerPolicy?: boolean | any;
|
|
242
|
+
noIndex?: boolean;
|
|
243
|
+
permissionsPolicy?: boolean | string;
|
|
244
|
+
[key: string]: any;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function securityHeaders(options?: SecurityHeadersOptions): RequestHandler;
|
|
248
|
+
export function createSecurityHeaders(options?: SecurityHeadersOptions): RequestHandler;
|
|
249
|
+
|
|
250
|
+
// Request Validator
|
|
251
|
+
export interface ValidationSchemas {
|
|
252
|
+
body?: any;
|
|
253
|
+
query?: any;
|
|
254
|
+
params?: any;
|
|
255
|
+
headers?: any;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface RequestValidatorOptions {
|
|
259
|
+
assignToReq?: boolean;
|
|
260
|
+
statusCode?: number;
|
|
261
|
+
message?: string;
|
|
262
|
+
handler?: (req: Request, res: Response, error: ValidationError) => void;
|
|
263
|
+
logger?: SecurityLogger;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function requestValidator(schemas: ValidationSchemas | any, options?: RequestValidatorOptions): RequestHandler;
|
|
267
|
+
export function createRequestValidator(schemas: ValidationSchemas | any, options?: RequestValidatorOptions): RequestHandler;
|
|
268
|
+
|
|
269
|
+
// Threat Detector
|
|
270
|
+
export interface ThreatRule {
|
|
271
|
+
id: string;
|
|
272
|
+
name: string;
|
|
273
|
+
pattern: RegExp | ((val: any, path: string) => boolean);
|
|
274
|
+
severity: 'high' | 'medium' | 'low';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface ThreatMatch {
|
|
278
|
+
ruleId: string;
|
|
279
|
+
ruleName: string;
|
|
280
|
+
path: string;
|
|
281
|
+
snippet: string;
|
|
282
|
+
severity: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface ThreatDetectorOptions {
|
|
286
|
+
action?: 'reject' | 'log' | 'pass';
|
|
287
|
+
statusCode?: number;
|
|
288
|
+
message?: string;
|
|
289
|
+
inspectUrl?: boolean;
|
|
290
|
+
inspectQuery?: boolean;
|
|
291
|
+
inspectBody?: boolean;
|
|
292
|
+
inspectHeaders?: boolean;
|
|
293
|
+
whitelistedPaths?: Array<string | RegExp>;
|
|
294
|
+
whitelistedKeys?: string[];
|
|
295
|
+
customRules?: ThreatRule[];
|
|
296
|
+
handler?: (req: Request, res: Response, error: ThreatDetectedError) => void;
|
|
297
|
+
logger?: SecurityLogger;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function threatDetector(options?: ThreatDetectorOptions): RequestHandler;
|
|
301
|
+
export function createThreatDetector(options?: ThreatDetectorOptions): RequestHandler;
|
|
302
|
+
export const DEFAULT_THREAT_PATTERNS: ThreatRule[];
|
|
303
|
+
export function scanValue(value: any, rules: ThreatRule[], currentPath?: string, whitelistedKeys?: string[]): ThreatMatch[];
|
|
304
|
+
|
|
305
|
+
// Helper
|
|
306
|
+
export function sendSecurityResponse(
|
|
307
|
+
req: Request,
|
|
308
|
+
res: Response,
|
|
309
|
+
error: SecurityError | Error | string,
|
|
310
|
+
statusCode?: number,
|
|
311
|
+
customHandler?: (req: Request, res: Response, error: any) => void
|
|
312
|
+
): void;
|
|
313
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base security error class.
|
|
5
|
+
*/
|
|
6
|
+
class SecurityError extends Error {
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} message - Human-readable error message.
|
|
9
|
+
* @param {number} [statusCode=500] - HTTP status code.
|
|
10
|
+
* @param {string} [code='SECURITY_ERROR'] - Machine-readable error code.
|
|
11
|
+
* @param {Record<string, any>} [details=null] - Optional structured error details.
|
|
12
|
+
*/
|
|
13
|
+
constructor(message, statusCode = 500, code = 'SECURITY_ERROR', details = null) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = this.constructor.name;
|
|
16
|
+
this.statusCode = statusCode;
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.details = details;
|
|
19
|
+
if (Error.captureStackTrace) {
|
|
20
|
+
Error.captureStackTrace(this, this.constructor);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
toJSON() {
|
|
25
|
+
return {
|
|
26
|
+
success: false,
|
|
27
|
+
error: {
|
|
28
|
+
code: this.code,
|
|
29
|
+
message: this.message,
|
|
30
|
+
...(this.details ? { details: this.details } : {})
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class RateLimitError extends SecurityError {
|
|
37
|
+
constructor(message = 'Too many requests, please try again later.', details = null) {
|
|
38
|
+
super(message, 429, 'RATE_LIMIT_EXCEEDED', details);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class AuthenticationError extends SecurityError {
|
|
43
|
+
constructor(message = 'Unauthorized: Invalid or missing API key.', details = null) {
|
|
44
|
+
super(message, 401, 'UNAUTHORIZED', details);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class ForbiddenError extends SecurityError {
|
|
49
|
+
constructor(message = 'Forbidden: Access denied.', details = null) {
|
|
50
|
+
super(message, 403, 'FORBIDDEN', details);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class PayloadTooLargeError extends SecurityError {
|
|
55
|
+
constructor(message = 'Payload Too Large: Request body exceeds maximum allowed size.', details = null) {
|
|
56
|
+
super(message, 413, 'PAYLOAD_TOO_LARGE', details);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class ValidationError extends SecurityError {
|
|
61
|
+
constructor(message = 'Validation failed: Invalid request parameters.', details = null) {
|
|
62
|
+
super(message, 400, 'VALIDATION_ERROR', details);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class ThreatDetectedError extends SecurityError {
|
|
67
|
+
constructor(message = 'Forbidden: Suspicious request pattern detected.', statusCode = 403, details = null) {
|
|
68
|
+
super(message, statusCode, 'THREAT_DETECTED', details);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class ConfigurationError extends SecurityError {
|
|
73
|
+
constructor(message, details = null) {
|
|
74
|
+
super(message, 500, 'CONFIGURATION_ERROR', details);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
SecurityError,
|
|
80
|
+
RateLimitError,
|
|
81
|
+
AuthenticationError,
|
|
82
|
+
ForbiddenError,
|
|
83
|
+
PayloadTooLargeError,
|
|
84
|
+
ValidationError,
|
|
85
|
+
ThreatDetectedError,
|
|
86
|
+
ConfigurationError
|
|
87
|
+
};
|
|
88
|
+
|