z-shield 0.1.1 → 0.1.2
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/README.md +123 -6
- package/dist/block-manager.d.ts +134 -0
- package/dist/block-manager.d.ts.map +1 -0
- package/dist/block-manager.js +425 -0
- package/dist/block-manager.js.map +1 -0
- package/dist/cli.d.ts +141 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +784 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +148 -0
- package/dist/config.js.map +1 -0
- package/dist/detector.d.ts +56 -0
- package/dist/detector.d.ts.map +1 -0
- package/dist/detector.js +81 -0
- package/dist/detector.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +141 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +347 -0
- package/dist/logger.js.map +1 -0
- package/dist/middleware.d.ts +68 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +272 -0
- package/dist/middleware.js.map +1 -0
- package/dist/next.d.ts +73 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +486 -0
- package/dist/next.js.map +1 -0
- package/dist/rate-limiter.d.ts +75 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +153 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/types.d.ts +199 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/next.d.ts +1 -0
- package/next.js +2 -0
- package/package.json +34 -5
- package/src/block-manager.js +0 -326
- package/src/cli.js +0 -764
- package/src/config.js +0 -165
- package/src/detector.js +0 -88
- package/src/index.js +0 -97
- package/src/logger.js +0 -280
- package/src/middleware.js +0 -288
- package/src/rate-limiter.js +0 -186
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport type { ZShieldConfig, ZShieldOptions } from './types';\n\nexport interface LoggerOptions {\n appName?: string;\n logDir?: string;\n logFilename?: string;\n maxLogSizeBytes?: number;\n maxLogFiles?: number;\n maxLogQueueSize?: number;\n maxQueueSize?: number;\n onLogError?: ((err: Error, event: Record<string, unknown>) => void) | null;\n}\n\nexport interface FormattedSecurityEvent {\n timestamp: string;\n appName: string;\n type: string;\n severity: string;\n ip: string;\n method: string;\n route: string;\n reason: string;\n action: string;\n}\n\nconst ANSI_REGEX =\n /[\\u001B\\u009B](?:\\[[0-9;?]*[ -/]*[@-~]|\\][^\\x07\\x1b]*(?:\\x07|\\x1b\\\\)|[@-Z\\\\-_])/g;\n\n/**\n * Strips ANSI terminal escape codes from untrusted strings to prevent terminal injection.\n * Removes CSI color/control sequences, OSC title/hyperlink sequences, and escape codes.\n *\n * @param str - Untrusted string\n * @returns Clean string without ANSI escapes\n */\nexport function stripAnsi(str: string): string {\n if (typeof str !== 'string') {\n return '';\n }\n return str.replace(ANSI_REGEX, '');\n}\n\n/**\n * Sanitizes an untrusted string to prevent Log Injection (CWE-117).\n * - Strips ANSI escapes\n * - Strips control characters (newlines \\r, \\n, null bytes, backspaces, tabs)\n * - Truncates to maxLength to prevent disk space exhaustion\n *\n * @param val - Untrusted candidate value\n * @param maxLength - Maximum permitted length\n * @returns Sanitized string\n */\nexport function sanitizeString(val: unknown, maxLength: number = 250): string {\n if (val === null || val === undefined) {\n return '';\n }\n let str = String(val);\n str = stripAnsi(str);\n // Replace control characters (ASCII 0-31, 127) with a clean space\n str = str.replace(/[\\x00-\\x1f\\x7f]/g, ' ').trim();\n // Collapse consecutive spaces\n str = str.replace(/\\s+/g, ' ');\n return str.slice(0, maxLength);\n}\n\n/**\n * Sanitizes HTTP route paths.\n * @param route - Untrusted route string\n * @param maxLength - Maximum permitted length\n * @returns Sanitized route\n */\nexport function sanitizeRoute(route: unknown, maxLength: number = 500): string {\n if (!route || typeof route !== 'string') {\n return '/';\n }\n let clean = route.split(/[\\r\\n]/)[0] ?? '';\n clean = stripAnsi(clean);\n // Strip query parameters and URL fragments to prevent credential leakage (CWE-532)\n clean = (clean.split('?')[0] ?? '').split('#')[0] ?? '';\n // Remove dangerous control characters and newlines\n clean = clean.replace(/[\\x00-\\x1f\\x7f]/g, '').trim();\n if (!clean.startsWith('/')) {\n clean = '/' + clean;\n }\n return clean.slice(0, maxLength) || '/';\n}\n\n/**\n * Sanitizes HTTP methods.\n * @param method - Untrusted method string\n * @returns Sanitized uppercase HTTP method\n */\nexport function sanitizeMethod(method: unknown): string {\n if (!method || typeof method !== 'string') {\n return 'GET';\n }\n const clean = method.toUpperCase().replace(/[^A-Z]/g, '');\n return clean.slice(0, 10) || 'GET';\n}\n\n/**\n * Logger - Safe, Persistent Local JSON Lines Security Event Logger\n *\n * Design Features:\n * - JSON Lines (.jsonl): Every event is on exactly one line ending in \\n.\n * - Non-Blocking & Asynchronous: Uses node:fs/promises without blocking the event loop.\n * - Strict Sanitization: Eliminates Log Injection (CWE-117) by stripping newlines and ANSI escapes.\n * - Privacy-Preserving: Never logs passwords, cookies, authorization headers, or request bodies (CWE-532).\n * - Bounded Storage: Automatic log rotation and retention limits prevent disk space exhaustion.\n * - Bounded Queue: Strictly caps pending in-memory write operations to prevent memory exhaustion under attack.\n * - Resilient: Filesystem errors (ENOSPC, EACCES) are safely caught and will not crash the host app.\n * - Sequential Queue: Guarantees concurrency safety so simultaneous writes never corrupt files.\n *\n * Hosting Notice:\n * Logs are stored in local persistent storage. If deployed to ephemeral platforms\n * (e.g. basic Heroku dynos, AWS Lambda, or serverless containers without mounted volumes),\n * local log files may be lost upon container restart or redeployment.\n */\nexport class Logger {\n readonly appName: string;\n readonly logDir: string;\n readonly logFilename: string;\n readonly filePath: string;\n readonly maxLogSizeBytes: number;\n readonly maxLogFiles: number;\n readonly maxQueueSize: number;\n readonly onLogError: ((err: Error, event: Record<string, unknown>) => void) | null;\n\n private _writeQueue: Promise<void>;\n private _dirEnsured: boolean;\n private _pendingWrites: number;\n private _droppedEvents: number;\n\n /**\n * @param config - Logger options or resolved Z-Shield configuration\n */\n constructor(config: LoggerOptions | ZShieldConfig | ZShieldOptions = {}) {\n this.appName = config.appName || 'default-app';\n this.logDir = path.resolve(config.logDir || path.join(process.cwd(), '.z-shield-logs'));\n this.logFilename = config.logFilename || 'events.jsonl';\n this.filePath = path.join(this.logDir, this.logFilename);\n this.maxLogSizeBytes = config.maxLogSizeBytes || 5 * 1024 * 1024; // 5MB\n this.maxLogFiles = typeof config.maxLogFiles === 'number' ? config.maxLogFiles : 5;\n const rawQueueLimit =\n (config as Record<string, unknown>).maxLogQueueSize ??\n (config as Record<string, unknown>).maxQueueSize;\n this.maxQueueSize =\n typeof rawQueueLimit === 'number' && Number.isInteger(rawQueueLimit) && rawQueueLimit > 0\n ? rawQueueLimit\n : 1000;\n this.onLogError = typeof config.onLogError === 'function' ? config.onLogError : null;\n\n this._writeQueue = Promise.resolve();\n this._dirEnsured = false;\n this._pendingWrites = 0;\n this._droppedEvents = 0;\n }\n\n /**\n * Returns the count of currently queued or executing asynchronous write operations.\n */\n get pendingWrites(): number {\n return this._pendingWrites;\n }\n\n /**\n * Returns the cumulative count of log events dropped due to write queue overload.\n */\n get droppedEvents(): number {\n return this._droppedEvents;\n }\n\n /**\n * Formats and sanitizes a security event into a validated schema.\n * Deliberately omits request bodies, authorization headers, cookies, and tokens.\n *\n * @param rawEvent - Raw event object\n * @returns Validated security event\n */\n formatEvent(rawEvent: Record<string, unknown> = {}): FormattedSecurityEvent {\n const rawTimestamp = rawEvent.timestamp;\n const now = typeof rawTimestamp === 'string' || typeof rawTimestamp === 'number'\n ? new Date(rawTimestamp)\n : new Date();\n const timestamp = isNaN(now.getTime()) ? new Date().toISOString() : now.toISOString();\n\n const allowedSeverities = ['info', 'warning', 'critical'];\n const rawSeverity = typeof rawEvent.severity === 'string' ? rawEvent.severity : '';\n const severity = allowedSeverities.includes(rawSeverity) ? rawSeverity : 'warning';\n\n const allowedActions = ['blocked', 'warned', 'allowed', 'unblocked'];\n const rawAction = typeof rawEvent.action === 'string' ? rawEvent.action : '';\n const action = allowedActions.includes(rawAction) ? rawAction : 'warned';\n\n return {\n timestamp,\n appName: sanitizeString(rawEvent.appName || this.appName, 50),\n type: sanitizeString(rawEvent.type || 'security_event', 30),\n severity,\n ip: sanitizeString(rawEvent.ip || 'unknown', 45),\n method: sanitizeMethod(rawEvent.method),\n route: sanitizeRoute(rawEvent.route),\n reason: sanitizeString(rawEvent.reason || 'Unspecified security event', 300),\n action\n };\n }\n\n /**\n * Safely logs a security event asynchronously.\n * Resolves to true if write succeeded, or false if it failed or was dropped due to queue overload.\n *\n * @param rawEvent - Raw event data\n * @returns True if write succeeded, false otherwise\n */\n log(rawEvent: Record<string, unknown>): Promise<boolean> {\n if (this._pendingWrites >= this.maxQueueSize) {\n this._droppedEvents++;\n if (this.onLogError) {\n try {\n this.onLogError(\n new Error(`[Z-Shield Logger]: Write queue capacity of ${this.maxQueueSize} reached. Event dropped.`),\n rawEvent\n );\n } catch {\n // Prevent user callback exceptions from propagating or crashing\n }\n } else if (this._droppedEvents === 1 || this._droppedEvents % 1000 === 0) {\n console.error(\n `[Z-Shield Logger Warning]: Write queue capacity of ${this.maxQueueSize} reached. ${this._droppedEvents} event(s) dropped.`\n );\n }\n return Promise.resolve(false);\n }\n\n this._pendingWrites++;\n return new Promise((resolve) => {\n this._writeQueue = this._writeQueue\n .then(() => this._writeEvent(rawEvent))\n .then(() => {\n this._pendingWrites--;\n resolve(true);\n })\n .catch((err: Error) => {\n this._pendingWrites--;\n this._handleError(err, rawEvent);\n resolve(false);\n });\n });\n }\n\n /**\n * Waits for all currently queued asynchronous log writes to complete.\n * Useful for deterministic testing, graceful shutdowns, and process exit handlers.\n */\n async flush(): Promise<void> {\n await this._writeQueue;\n }\n\n /**\n * Internal write operation executed within the sequential queue.\n *\n * @param rawEvent - Raw event to serialize and write\n */\n private async _writeEvent(rawEvent: Record<string, unknown>): Promise<void> {\n const event = this.formatEvent(rawEvent);\n await this._ensureDir();\n await this._rotateIfNeeded();\n\n const line = JSON.stringify(event) + '\\n';\n try {\n await fs.promises.appendFile(this.filePath, line, { encoding: 'utf8', flag: 'a' });\n } catch (err: unknown) {\n // Self-healing recovery: if directory was removed externally right before write,\n // recreate directory and retry appending once before throwing.\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n this._dirEnsured = false;\n await fs.promises.mkdir(this.logDir, { recursive: true });\n this._dirEnsured = true;\n await fs.promises.appendFile(this.filePath, line, { encoding: 'utf8', flag: 'a' });\n return;\n }\n throw err;\n }\n }\n\n /**\n * Ensures the target log directory exists.\n */\n private async _ensureDir(): Promise<void> {\n if (!this._dirEnsured || !fs.existsSync(this.logDir)) {\n await fs.promises.mkdir(this.logDir, { recursive: true });\n this._dirEnsured = true;\n }\n }\n\n /**\n * Checks file size and rotates log files if size exceeds maxLogSizeBytes.\n */\n private async _rotateIfNeeded(): Promise<void> {\n try {\n const stats = await fs.promises.stat(this.filePath);\n if (stats.size < this.maxLogSizeBytes) {\n return;\n }\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n return; // File does not exist yet\n }\n throw err;\n }\n\n const { name, ext } = path.parse(this.logFilename);\n\n // If maxLogFiles is 0, simply remove the current file\n if (this.maxLogFiles === 0) {\n try {\n await fs.promises.unlink(this.filePath);\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;\n }\n return;\n }\n\n // Shift existing rotated files:\n // e.g. events.5.jsonl deleted (retention limit)\n // events.4.jsonl -> events.5.jsonl\n // events.1.jsonl -> events.2.jsonl\n for (let i = this.maxLogFiles; i >= 1; i--) {\n const currentRotated = path.join(this.logDir, `${name}.${i}${ext}`);\n if (i === this.maxLogFiles) {\n try {\n await fs.promises.unlink(currentRotated);\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;\n }\n } else {\n const nextRotated = path.join(this.logDir, `${name}.${i + 1}${ext}`);\n try {\n await fs.promises.rename(currentRotated, nextRotated);\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;\n }\n }\n }\n\n // Move current events.jsonl to events.1.jsonl\n const firstRotated = path.join(this.logDir, `${name}.1${ext}`);\n try {\n await fs.promises.rename(this.filePath, firstRotated);\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;\n }\n }\n\n /**\n * Safely handles write and filesystem failures without crashing the application.\n *\n * @param err - Encountered error\n * @param rawEvent - Raw event data attempted to be written\n */\n private _handleError(err: Error, rawEvent: Record<string, unknown>): void {\n // If the directory was deleted externally while running, reset _dirEnsured so the next\n // write attempt will automatically recreate the directory instead of failing permanently.\n this._dirEnsured = false;\n\n if (this.onLogError) {\n try {\n this.onLogError(err, rawEvent);\n } catch {\n // Prevent errors in the user's callback from crashing the app\n }\n } else {\n // Print clear, non-fatal warning to stderr\n console.error(`[Z-Shield Logger Warning]: Failed to write security event log: ${err.message}`);\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { RateLimiter } from './rate-limiter';
|
|
2
|
+
import { Detector } from './detector';
|
|
3
|
+
import { BlockManager } from './block-manager';
|
|
4
|
+
import { Logger } from './logger';
|
|
5
|
+
import type { ExpressLikeRequest, ExpressLikeResponse, ExpressMiddleware, ZShieldConfig, ZShieldOptions } from './types';
|
|
6
|
+
declare global {
|
|
7
|
+
namespace Express {
|
|
8
|
+
interface Request {
|
|
9
|
+
zShield?: {
|
|
10
|
+
ip: string;
|
|
11
|
+
mode: string;
|
|
12
|
+
appName: string;
|
|
13
|
+
isExcluded: boolean;
|
|
14
|
+
startTime: number;
|
|
15
|
+
detection: unknown;
|
|
16
|
+
blocked: boolean;
|
|
17
|
+
blockReason?: string;
|
|
18
|
+
logPromise?: Promise<boolean> | null;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export interface MiddlewareDependencies {
|
|
24
|
+
rateLimiter?: RateLimiter;
|
|
25
|
+
detector?: Detector;
|
|
26
|
+
blockManager?: BlockManager;
|
|
27
|
+
logger?: Logger;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Safely extracts the client's IP address and normalizes IPv4 and IPv6 representations.
|
|
31
|
+
*
|
|
32
|
+
* Security Note:
|
|
33
|
+
* An attacker can easily send spoofed headers like `X-Forwarded-For: 1.2.3.4`.
|
|
34
|
+
* Therefore, we NEVER inspect `X-Forwarded-For` directly unless `trustProxy` is true
|
|
35
|
+
* AND Express itself has verified it through its own `trust proxy` configuration.
|
|
36
|
+
*
|
|
37
|
+
* @param req - The incoming Express request object
|
|
38
|
+
* @param trustProxy - Whether proxy headers are explicitly trusted
|
|
39
|
+
* @returns The normalized client IP address
|
|
40
|
+
*/
|
|
41
|
+
export declare function extractClientIp(req: ExpressLikeRequest, trustProxy: boolean): string;
|
|
42
|
+
/**
|
|
43
|
+
* Determines whether a given request path matches any of the excluded route patterns.
|
|
44
|
+
* Respects path-segment boundaries so that excluding '/health' matches '/health' or '/health/live'
|
|
45
|
+
* but does NOT match '/health-admin' or '/healthcare'.
|
|
46
|
+
* Also normalizes trailing slashes and ignores query strings and fragments.
|
|
47
|
+
*
|
|
48
|
+
* @param requestPath - The incoming request path (or raw url)
|
|
49
|
+
* @param excludeRoutes - Configured excluded route prefixes
|
|
50
|
+
* @returns True if route is excluded
|
|
51
|
+
*/
|
|
52
|
+
export declare function isRouteExcluded(requestPath: string, excludeRoutes: readonly string[]): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Safely terminates an open HTTP response without calling next() or setting headers.
|
|
55
|
+
* Used when a blocked request in protect mode already had its headers committed upstream.
|
|
56
|
+
*
|
|
57
|
+
* @param res - Response object
|
|
58
|
+
*/
|
|
59
|
+
export declare function safelyEndResponse(res: ExpressLikeResponse): void;
|
|
60
|
+
/**
|
|
61
|
+
* Factory function to create the Z-Shield Express middleware.
|
|
62
|
+
*
|
|
63
|
+
* @param options - Custom configuration options
|
|
64
|
+
* @param dependencies - Injected dependencies (useful for testing)
|
|
65
|
+
* @returns Express middleware function: (req, res, next) => void
|
|
66
|
+
*/
|
|
67
|
+
export declare function createMiddleware(options?: ZShieldOptions | ZShieldConfig, dependencies?: MiddlewareDependencies): ExpressMiddleware;
|
|
68
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,YAAY,EAA0B,MAAM,iBAAiB,CAAC;AACvE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EAEnB,iBAAiB,EACjB,aAAa,EACb,cAAc,EACf,MAAM,SAAS,CAAC;AAEjB,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,OAAO,CAAC,EAAE;gBACR,EAAE,EAAE,MAAM,CAAC;gBACX,IAAI,EAAE,MAAM,CAAC;gBACb,OAAO,EAAE,MAAM,CAAC;gBAChB,UAAU,EAAE,OAAO,CAAC;gBACpB,SAAS,EAAE,MAAM,CAAC;gBAClB,SAAS,EAAE,OAAO,CAAC;gBACnB,OAAO,EAAE,OAAO,CAAC;gBACjB,WAAW,CAAC,EAAE,MAAM,CAAC;gBACrB,UAAU,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;aACtC,CAAC;SACH;KACF;CACF;AAED,MAAM,WAAW,sBAAsB;IACrC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAkBD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,kBAAkB,EAAE,UAAU,EAAE,OAAO,GAAG,MAAM,CAYpF;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAgC9F;AAeD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,mBAAmB,GAAG,IAAI,CAIhE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,cAAc,GAAG,aAAkB,EAC5C,YAAY,GAAE,sBAA2B,GACxC,iBAAiB,CAuLnB"}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractClientIp = extractClientIp;
|
|
4
|
+
exports.isRouteExcluded = isRouteExcluded;
|
|
5
|
+
exports.safelyEndResponse = safelyEndResponse;
|
|
6
|
+
exports.createMiddleware = createMiddleware;
|
|
7
|
+
const config_1 = require("./config");
|
|
8
|
+
const rate_limiter_1 = require("./rate-limiter");
|
|
9
|
+
const detector_1 = require("./detector");
|
|
10
|
+
const block_manager_1 = require("./block-manager");
|
|
11
|
+
const logger_1 = require("./logger");
|
|
12
|
+
function hasLegacyConnection(req) {
|
|
13
|
+
return (typeof req === 'object' &&
|
|
14
|
+
req !== null &&
|
|
15
|
+
'connection' in req &&
|
|
16
|
+
typeof req.connection === 'object' &&
|
|
17
|
+
req.connection !== null);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Safely extracts the client's IP address and normalizes IPv4 and IPv6 representations.
|
|
21
|
+
*
|
|
22
|
+
* Security Note:
|
|
23
|
+
* An attacker can easily send spoofed headers like `X-Forwarded-For: 1.2.3.4`.
|
|
24
|
+
* Therefore, we NEVER inspect `X-Forwarded-For` directly unless `trustProxy` is true
|
|
25
|
+
* AND Express itself has verified it through its own `trust proxy` configuration.
|
|
26
|
+
*
|
|
27
|
+
* @param req - The incoming Express request object
|
|
28
|
+
* @param trustProxy - Whether proxy headers are explicitly trusted
|
|
29
|
+
* @returns The normalized client IP address
|
|
30
|
+
*/
|
|
31
|
+
function extractClientIp(req, trustProxy) {
|
|
32
|
+
let ip = '';
|
|
33
|
+
if (trustProxy && typeof req.ip === 'string' && req.ip.length > 0) {
|
|
34
|
+
ip = req.ip;
|
|
35
|
+
}
|
|
36
|
+
else if (req.socket && typeof req.socket.remoteAddress === 'string') {
|
|
37
|
+
ip = req.socket.remoteAddress;
|
|
38
|
+
}
|
|
39
|
+
else if (hasLegacyConnection(req) && typeof req.connection?.remoteAddress === 'string') {
|
|
40
|
+
ip = req.connection.remoteAddress;
|
|
41
|
+
}
|
|
42
|
+
return (0, block_manager_1.normalizeIp)(ip);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Determines whether a given request path matches any of the excluded route patterns.
|
|
46
|
+
* Respects path-segment boundaries so that excluding '/health' matches '/health' or '/health/live'
|
|
47
|
+
* but does NOT match '/health-admin' or '/healthcare'.
|
|
48
|
+
* Also normalizes trailing slashes and ignores query strings and fragments.
|
|
49
|
+
*
|
|
50
|
+
* @param requestPath - The incoming request path (or raw url)
|
|
51
|
+
* @param excludeRoutes - Configured excluded route prefixes
|
|
52
|
+
* @returns True if route is excluded
|
|
53
|
+
*/
|
|
54
|
+
function isRouteExcluded(requestPath, excludeRoutes) {
|
|
55
|
+
if (!Array.isArray(excludeRoutes) || excludeRoutes.length === 0) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
// Strip query string and fragment if present (e.g. from req.url)
|
|
59
|
+
const cleanPath = (requestPath || '/').split('?')[0]?.split('#')[0] ?? '/';
|
|
60
|
+
return excludeRoutes.some((route) => {
|
|
61
|
+
if (typeof route !== 'string' || route.length === 0) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (route === '/') {
|
|
65
|
+
return cleanPath === '/';
|
|
66
|
+
}
|
|
67
|
+
// Strip trailing slash from route prefix for normalized segment comparison
|
|
68
|
+
const normalizedPrefix = route.endsWith('/') ? route.slice(0, -1) : route;
|
|
69
|
+
// Exact match (e.g. '/health' === '/health' or '/status' === '/status')
|
|
70
|
+
if (cleanPath === normalizedPrefix) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
// Sub-path segment match (e.g. '/health/check'.startsWith('/health/'))
|
|
74
|
+
if (cleanPath.startsWith(normalizedPrefix + '/')) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Sets a response header only if headers have not already been sent to the client.
|
|
82
|
+
*
|
|
83
|
+
* @param res - Response object
|
|
84
|
+
* @param name - Header name
|
|
85
|
+
* @param value - Header value
|
|
86
|
+
*/
|
|
87
|
+
function safeSetHeader(res, name, value) {
|
|
88
|
+
if (res && !res.headersSent && typeof res.setHeader === 'function') {
|
|
89
|
+
res.setHeader(name, value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Safely terminates an open HTTP response without calling next() or setting headers.
|
|
94
|
+
* Used when a blocked request in protect mode already had its headers committed upstream.
|
|
95
|
+
*
|
|
96
|
+
* @param res - Response object
|
|
97
|
+
*/
|
|
98
|
+
function safelyEndResponse(res) {
|
|
99
|
+
if (res && !res.writableEnded && typeof res.end === 'function') {
|
|
100
|
+
res.end();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Factory function to create the Z-Shield Express middleware.
|
|
105
|
+
*
|
|
106
|
+
* @param options - Custom configuration options
|
|
107
|
+
* @param dependencies - Injected dependencies (useful for testing)
|
|
108
|
+
* @returns Express middleware function: (req, res, next) => void
|
|
109
|
+
*/
|
|
110
|
+
function createMiddleware(options = {}, dependencies = {}) {
|
|
111
|
+
const config = (0, config_1.resolveConfig)(options);
|
|
112
|
+
const rateLimiter = dependencies.rateLimiter || new rate_limiter_1.RateLimiter(config);
|
|
113
|
+
const detector = dependencies.detector || new detector_1.Detector({ rateLimiter, config });
|
|
114
|
+
const blockManager = dependencies.blockManager || new block_manager_1.BlockManager(config);
|
|
115
|
+
const logger = dependencies.logger || new logger_1.Logger(config);
|
|
116
|
+
return function zShieldMiddleware(req, res, next) {
|
|
117
|
+
try {
|
|
118
|
+
const clientIp = extractClientIp(req, config.trustProxy);
|
|
119
|
+
const rawPath = (typeof req.path === 'string' && req.path.length > 0)
|
|
120
|
+
? req.path
|
|
121
|
+
: (req.url || '/');
|
|
122
|
+
const requestPath = (rawPath.split('?')[0] ?? '').split('#')[0] || '/';
|
|
123
|
+
const requestMethod = req.method || 'GET';
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
// Check if this route is in the exclusion list (respecting segment boundaries)
|
|
126
|
+
const isExcluded = isRouteExcluded(requestPath, config.excludeRoutes);
|
|
127
|
+
if (isExcluded) {
|
|
128
|
+
req.zShield = {
|
|
129
|
+
ip: clientIp,
|
|
130
|
+
mode: config.mode,
|
|
131
|
+
appName: config.appName,
|
|
132
|
+
isExcluded: true,
|
|
133
|
+
startTime: now,
|
|
134
|
+
detection: null,
|
|
135
|
+
blocked: false
|
|
136
|
+
};
|
|
137
|
+
safeSetHeader(res, 'X-Z-Shield-Protected', '1');
|
|
138
|
+
safeSetHeader(res, 'X-Z-Shield-Mode', config.mode);
|
|
139
|
+
return next();
|
|
140
|
+
}
|
|
141
|
+
// Step 1: Check active temporary blocks
|
|
142
|
+
const isIpBlocked = blockManager.isBlocked(clientIp, now);
|
|
143
|
+
if (isIpBlocked) {
|
|
144
|
+
const blockInfo = blockManager.getBlockInfo(clientIp, now);
|
|
145
|
+
const reason = blockInfo ? blockInfo.reason : 'Administrative block';
|
|
146
|
+
const remainingMs = blockInfo ? blockInfo.remainingMs : 0;
|
|
147
|
+
const retryAfterSeconds = Math.max(1, Math.ceil(remainingMs / 1000));
|
|
148
|
+
const action = config.mode === 'protect' ? 'blocked' : 'warned';
|
|
149
|
+
// Log the security event asynchronously
|
|
150
|
+
const logPromise = logger.log({
|
|
151
|
+
appName: config.appName,
|
|
152
|
+
type: 'blocked_ip',
|
|
153
|
+
severity: 'warning',
|
|
154
|
+
ip: clientIp,
|
|
155
|
+
method: requestMethod,
|
|
156
|
+
route: requestPath,
|
|
157
|
+
reason,
|
|
158
|
+
action
|
|
159
|
+
});
|
|
160
|
+
req.zShield = {
|
|
161
|
+
ip: clientIp,
|
|
162
|
+
mode: config.mode,
|
|
163
|
+
appName: config.appName,
|
|
164
|
+
isExcluded: false,
|
|
165
|
+
startTime: now,
|
|
166
|
+
detection: null,
|
|
167
|
+
blocked: true,
|
|
168
|
+
blockReason: reason,
|
|
169
|
+
logPromise
|
|
170
|
+
};
|
|
171
|
+
safeSetHeader(res, 'X-Z-Shield-Protected', '1');
|
|
172
|
+
safeSetHeader(res, 'X-Z-Shield-Mode', config.mode);
|
|
173
|
+
safeSetHeader(res, 'X-Z-Shield-Blocked', '1');
|
|
174
|
+
safeSetHeader(res, 'X-Z-Shield-Reason', reason);
|
|
175
|
+
safeSetHeader(res, 'Retry-After', String(retryAfterSeconds));
|
|
176
|
+
if (config.mode === 'protect') {
|
|
177
|
+
if (!res.headersSent) {
|
|
178
|
+
if (typeof res.status === 'function') {
|
|
179
|
+
const statusRes = res.status(429);
|
|
180
|
+
if (statusRes && typeof statusRes.json === 'function') {
|
|
181
|
+
statusRes.json({
|
|
182
|
+
error: 'Too Many Requests',
|
|
183
|
+
message: 'Your IP address has been temporarily blocked due to suspicious activity.',
|
|
184
|
+
retryAfter: retryAfterSeconds
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// If headers were already committed upstream, we cannot send 429.
|
|
191
|
+
// Safely terminate the open response without calling next().
|
|
192
|
+
safelyEndResponse(res);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
// Monitor mode: mark warning header, but allow request through
|
|
196
|
+
safeSetHeader(res, 'X-Z-Shield-Suspicious', '1');
|
|
197
|
+
return next();
|
|
198
|
+
}
|
|
199
|
+
// Step 2: Rate limit tracking and abuse detection
|
|
200
|
+
const detection = detector.check(clientIp, req, now);
|
|
201
|
+
let logPromise = null;
|
|
202
|
+
if (detection.isSuspicious) {
|
|
203
|
+
const action = config.mode === 'protect' ? 'blocked' : 'warned';
|
|
204
|
+
logPromise = logger.log({
|
|
205
|
+
appName: config.appName,
|
|
206
|
+
type: detection.type || 'rate_limit',
|
|
207
|
+
severity: detection.severity || 'warning',
|
|
208
|
+
ip: clientIp,
|
|
209
|
+
method: requestMethod,
|
|
210
|
+
route: requestPath,
|
|
211
|
+
reason: detection.reason || 'Threshold exceeded',
|
|
212
|
+
action
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
req.zShield = {
|
|
216
|
+
ip: clientIp,
|
|
217
|
+
mode: config.mode,
|
|
218
|
+
appName: config.appName,
|
|
219
|
+
isExcluded: false,
|
|
220
|
+
startTime: now,
|
|
221
|
+
detection,
|
|
222
|
+
blocked: false,
|
|
223
|
+
logPromise
|
|
224
|
+
};
|
|
225
|
+
// Set informative headers
|
|
226
|
+
safeSetHeader(res, 'X-Z-Shield-Protected', '1');
|
|
227
|
+
safeSetHeader(res, 'X-Z-Shield-Mode', config.mode);
|
|
228
|
+
safeSetHeader(res, 'X-RateLimit-Limit', String(config.maxRequests));
|
|
229
|
+
safeSetHeader(res, 'X-RateLimit-Remaining', String(detection.remaining));
|
|
230
|
+
safeSetHeader(res, 'X-RateLimit-Reset', String(Math.ceil(detection.resetTime / 1000)));
|
|
231
|
+
if (detection.isSuspicious) {
|
|
232
|
+
safeSetHeader(res, 'X-Z-Shield-Suspicious', '1');
|
|
233
|
+
safeSetHeader(res, 'X-Z-Shield-Reason', detection.reason || 'Threshold exceeded');
|
|
234
|
+
if (config.mode === 'protect') {
|
|
235
|
+
// If autoBlock is enabled and IP is valid, add to block manager.
|
|
236
|
+
// Unknown or invalid IPs must not be passed to blockManager.block()
|
|
237
|
+
// as that would throw a TypeError and cause an unexpected HTTP 500.
|
|
238
|
+
if (config.autoBlock && (0, block_manager_1.isValidIp)(clientIp)) {
|
|
239
|
+
blockManager.block(clientIp, config.blockDurationMs, detection.reason || 'Threshold exceeded', now);
|
|
240
|
+
}
|
|
241
|
+
const retryAfterSeconds = Math.max(1, Math.ceil((detection.resetTime - now) / 1000));
|
|
242
|
+
safeSetHeader(res, 'Retry-After', String(retryAfterSeconds));
|
|
243
|
+
if (!res.headersSent) {
|
|
244
|
+
if (typeof res.status === 'function') {
|
|
245
|
+
const statusRes = res.status(429);
|
|
246
|
+
if (statusRes && typeof statusRes.json === 'function') {
|
|
247
|
+
statusRes.json({
|
|
248
|
+
error: 'Too Many Requests',
|
|
249
|
+
message: 'Request threshold exceeded. Please try again later.',
|
|
250
|
+
retryAfter: retryAfterSeconds
|
|
251
|
+
});
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// If headers were already committed upstream, we cannot send 429.
|
|
257
|
+
// Safely terminate the open response without calling next().
|
|
258
|
+
safelyEndResponse(res);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// In monitor mode or normal traffic in protect mode: allow through
|
|
263
|
+
return next();
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
// If an unexpected error occurs in middleware execution, pass it safely to Express
|
|
267
|
+
// error handling without crashing the entire Node.js server process.
|
|
268
|
+
return next(err);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { resolveConfig } from './config';\nimport { RateLimiter } from './rate-limiter';\nimport { Detector } from './detector';\nimport { BlockManager, isValidIp, normalizeIp } from './block-manager';\nimport { Logger } from './logger';\nimport type {\n ExpressLikeRequest,\n ExpressLikeResponse,\n ExpressLikeNextFunction,\n ExpressMiddleware,\n ZShieldConfig,\n ZShieldOptions\n} from './types';\n\ndeclare global {\n namespace Express {\n interface Request {\n zShield?: {\n ip: string;\n mode: string;\n appName: string;\n isExcluded: boolean;\n startTime: number;\n detection: unknown;\n blocked: boolean;\n blockReason?: string;\n logPromise?: Promise<boolean> | null;\n };\n }\n }\n}\n\nexport interface MiddlewareDependencies {\n rateLimiter?: RateLimiter;\n detector?: Detector;\n blockManager?: BlockManager;\n logger?: Logger;\n}\n\ninterface LegacyConnectionHolder {\n connection?: {\n remoteAddress?: string;\n };\n}\n\nfunction hasLegacyConnection(req: unknown): req is LegacyConnectionHolder {\n return (\n typeof req === 'object' &&\n req !== null &&\n 'connection' in req &&\n typeof (req as LegacyConnectionHolder).connection === 'object' &&\n (req as LegacyConnectionHolder).connection !== null\n );\n}\n\n/**\n * Safely extracts the client's IP address and normalizes IPv4 and IPv6 representations.\n *\n * Security Note:\n * An attacker can easily send spoofed headers like `X-Forwarded-For: 1.2.3.4`.\n * Therefore, we NEVER inspect `X-Forwarded-For` directly unless `trustProxy` is true\n * AND Express itself has verified it through its own `trust proxy` configuration.\n *\n * @param req - The incoming Express request object\n * @param trustProxy - Whether proxy headers are explicitly trusted\n * @returns The normalized client IP address\n */\nexport function extractClientIp(req: ExpressLikeRequest, trustProxy: boolean): string {\n let ip = '';\n\n if (trustProxy && typeof req.ip === 'string' && req.ip.length > 0) {\n ip = req.ip;\n } else if (req.socket && typeof req.socket.remoteAddress === 'string') {\n ip = req.socket.remoteAddress;\n } else if (hasLegacyConnection(req) && typeof req.connection?.remoteAddress === 'string') {\n ip = req.connection.remoteAddress;\n }\n\n return normalizeIp(ip);\n}\n\n/**\n * Determines whether a given request path matches any of the excluded route patterns.\n * Respects path-segment boundaries so that excluding '/health' matches '/health' or '/health/live'\n * but does NOT match '/health-admin' or '/healthcare'.\n * Also normalizes trailing slashes and ignores query strings and fragments.\n *\n * @param requestPath - The incoming request path (or raw url)\n * @param excludeRoutes - Configured excluded route prefixes\n * @returns True if route is excluded\n */\nexport function isRouteExcluded(requestPath: string, excludeRoutes: readonly string[]): boolean {\n if (!Array.isArray(excludeRoutes) || excludeRoutes.length === 0) {\n return false;\n }\n\n // Strip query string and fragment if present (e.g. from req.url)\n const cleanPath = (requestPath || '/').split('?')[0]?.split('#')[0] ?? '/';\n\n return excludeRoutes.some((route) => {\n if (typeof route !== 'string' || route.length === 0) {\n return false;\n }\n\n if (route === '/') {\n return cleanPath === '/';\n }\n\n // Strip trailing slash from route prefix for normalized segment comparison\n const normalizedPrefix = route.endsWith('/') ? route.slice(0, -1) : route;\n\n // Exact match (e.g. '/health' === '/health' or '/status' === '/status')\n if (cleanPath === normalizedPrefix) {\n return true;\n }\n\n // Sub-path segment match (e.g. '/health/check'.startsWith('/health/'))\n if (cleanPath.startsWith(normalizedPrefix + '/')) {\n return true;\n }\n\n return false;\n });\n}\n\n/**\n * Sets a response header only if headers have not already been sent to the client.\n *\n * @param res - Response object\n * @param name - Header name\n * @param value - Header value\n */\nfunction safeSetHeader(res: ExpressLikeResponse, name: string, value: string): void {\n if (res && !res.headersSent && typeof res.setHeader === 'function') {\n res.setHeader(name, value);\n }\n}\n\n/**\n * Safely terminates an open HTTP response without calling next() or setting headers.\n * Used when a blocked request in protect mode already had its headers committed upstream.\n *\n * @param res - Response object\n */\nexport function safelyEndResponse(res: ExpressLikeResponse): void {\n if (res && !res.writableEnded && typeof res.end === 'function') {\n res.end();\n }\n}\n\n/**\n * Factory function to create the Z-Shield Express middleware.\n *\n * @param options - Custom configuration options\n * @param dependencies - Injected dependencies (useful for testing)\n * @returns Express middleware function: (req, res, next) => void\n */\nexport function createMiddleware(\n options: ZShieldOptions | ZShieldConfig = {},\n dependencies: MiddlewareDependencies = {}\n): ExpressMiddleware {\n const config = resolveConfig(options);\n const rateLimiter = dependencies.rateLimiter || new RateLimiter(config);\n const detector = dependencies.detector || new Detector({ rateLimiter, config });\n const blockManager = dependencies.blockManager || new BlockManager(config);\n const logger = dependencies.logger || new Logger(config);\n\n return function zShieldMiddleware(\n req: ExpressLikeRequest,\n res: ExpressLikeResponse,\n next: ExpressLikeNextFunction\n ): void {\n try {\n const clientIp = extractClientIp(req, config.trustProxy);\n const rawPath = (typeof req.path === 'string' && req.path.length > 0)\n ? req.path\n : (req.url || '/');\n const requestPath = (rawPath.split('?')[0] ?? '').split('#')[0] || '/';\n const requestMethod = req.method || 'GET';\n const now = Date.now();\n\n // Check if this route is in the exclusion list (respecting segment boundaries)\n const isExcluded = isRouteExcluded(requestPath, config.excludeRoutes);\n\n if (isExcluded) {\n req.zShield = {\n ip: clientIp,\n mode: config.mode,\n appName: config.appName,\n isExcluded: true,\n startTime: now,\n detection: null,\n blocked: false\n };\n safeSetHeader(res, 'X-Z-Shield-Protected', '1');\n safeSetHeader(res, 'X-Z-Shield-Mode', config.mode);\n return next();\n }\n\n // Step 1: Check active temporary blocks\n const isIpBlocked = blockManager.isBlocked(clientIp, now);\n\n if (isIpBlocked) {\n const blockInfo = blockManager.getBlockInfo(clientIp, now);\n const reason = blockInfo ? blockInfo.reason : 'Administrative block';\n const remainingMs = blockInfo ? blockInfo.remainingMs : 0;\n const retryAfterSeconds = Math.max(1, Math.ceil(remainingMs / 1000));\n const action = config.mode === 'protect' ? 'blocked' : 'warned';\n\n // Log the security event asynchronously\n const logPromise = logger.log({\n appName: config.appName,\n type: 'blocked_ip',\n severity: 'warning',\n ip: clientIp,\n method: requestMethod,\n route: requestPath,\n reason,\n action\n });\n\n req.zShield = {\n ip: clientIp,\n mode: config.mode,\n appName: config.appName,\n isExcluded: false,\n startTime: now,\n detection: null,\n blocked: true,\n blockReason: reason,\n logPromise\n };\n\n safeSetHeader(res, 'X-Z-Shield-Protected', '1');\n safeSetHeader(res, 'X-Z-Shield-Mode', config.mode);\n safeSetHeader(res, 'X-Z-Shield-Blocked', '1');\n safeSetHeader(res, 'X-Z-Shield-Reason', reason);\n safeSetHeader(res, 'Retry-After', String(retryAfterSeconds));\n\n if (config.mode === 'protect') {\n if (!res.headersSent) {\n if (typeof res.status === 'function') {\n const statusRes = res.status(429);\n if (statusRes && typeof statusRes.json === 'function') {\n statusRes.json({\n error: 'Too Many Requests',\n message: 'Your IP address has been temporarily blocked due to suspicious activity.',\n retryAfter: retryAfterSeconds\n });\n return;\n }\n }\n }\n // If headers were already committed upstream, we cannot send 429.\n // Safely terminate the open response without calling next().\n safelyEndResponse(res);\n return;\n }\n\n // Monitor mode: mark warning header, but allow request through\n safeSetHeader(res, 'X-Z-Shield-Suspicious', '1');\n return next();\n }\n\n // Step 2: Rate limit tracking and abuse detection\n const detection = detector.check(clientIp, req, now);\n\n let logPromise: Promise<boolean> | null = null;\n if (detection.isSuspicious) {\n const action = config.mode === 'protect' ? 'blocked' : 'warned';\n logPromise = logger.log({\n appName: config.appName,\n type: detection.type || 'rate_limit',\n severity: detection.severity || 'warning',\n ip: clientIp,\n method: requestMethod,\n route: requestPath,\n reason: detection.reason || 'Threshold exceeded',\n action\n });\n }\n\n req.zShield = {\n ip: clientIp,\n mode: config.mode,\n appName: config.appName,\n isExcluded: false,\n startTime: now,\n detection,\n blocked: false,\n logPromise\n };\n\n // Set informative headers\n safeSetHeader(res, 'X-Z-Shield-Protected', '1');\n safeSetHeader(res, 'X-Z-Shield-Mode', config.mode);\n safeSetHeader(res, 'X-RateLimit-Limit', String(config.maxRequests));\n safeSetHeader(res, 'X-RateLimit-Remaining', String(detection.remaining));\n safeSetHeader(res, 'X-RateLimit-Reset', String(Math.ceil(detection.resetTime / 1000)));\n\n if (detection.isSuspicious) {\n safeSetHeader(res, 'X-Z-Shield-Suspicious', '1');\n safeSetHeader(res, 'X-Z-Shield-Reason', detection.reason || 'Threshold exceeded');\n\n if (config.mode === 'protect') {\n // If autoBlock is enabled and IP is valid, add to block manager.\n // Unknown or invalid IPs must not be passed to blockManager.block()\n // as that would throw a TypeError and cause an unexpected HTTP 500.\n if (config.autoBlock && isValidIp(clientIp)) {\n blockManager.block(clientIp, config.blockDurationMs, detection.reason || 'Threshold exceeded', now);\n }\n\n const retryAfterSeconds = Math.max(1, Math.ceil((detection.resetTime - now) / 1000));\n safeSetHeader(res, 'Retry-After', String(retryAfterSeconds));\n\n if (!res.headersSent) {\n if (typeof res.status === 'function') {\n const statusRes = res.status(429);\n if (statusRes && typeof statusRes.json === 'function') {\n statusRes.json({\n error: 'Too Many Requests',\n message: 'Request threshold exceeded. Please try again later.',\n retryAfter: retryAfterSeconds\n });\n return;\n }\n }\n }\n // If headers were already committed upstream, we cannot send 429.\n // Safely terminate the open response without calling next().\n safelyEndResponse(res);\n return;\n }\n }\n\n // In monitor mode or normal traffic in protect mode: allow through\n return next();\n } catch (err) {\n // If an unexpected error occurs in middleware execution, pass it safely to Express\n // error handling without crashing the entire Node.js server process.\n return next(err);\n }\n };\n}\n\n"]}
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js App Router Compatibility Adapter for Z-Shield.
|
|
3
|
+
*
|
|
4
|
+
* Provides Web-standard Request and Response security middleware for Next.js App Router
|
|
5
|
+
* route handlers (e.g. app/api/.../route.ts).
|
|
6
|
+
*
|
|
7
|
+
* Runtime requirement: Node.js runtime (`export const runtime = 'nodejs';`).
|
|
8
|
+
* State note: In-memory rate limiting and blocking state is process-local.
|
|
9
|
+
*/
|
|
10
|
+
import type { ZShieldConfig, ZShieldInstance, NextLikeRequest, NextShieldOptions, NextShieldCheckResult, NextShieldInstance, NextRouteHandler, NextClientIpStrategy, NextClientIpExtractor } from './types';
|
|
11
|
+
/**
|
|
12
|
+
* Extracts and normalizes the client IP address from a Web-standard or Next.js Request.
|
|
13
|
+
*
|
|
14
|
+
* Security Model:
|
|
15
|
+
* 1. When `trustProxy` is false:
|
|
16
|
+
* - All reverse-proxy headers (X-Forwarded-For, X-Real-IP, CF-Connecting-IP, etc.)
|
|
17
|
+
* and unverified platform properties are strictly ignored to prevent client IP spoofing attacks.
|
|
18
|
+
* - Only a verified Node.js TCP socket remote address (`socket.remoteAddress`) or an
|
|
19
|
+
* explicitly provided custom extractor function is accepted.
|
|
20
|
+
* - In environments without a direct TCP socket (e.g. serverless Web Requests without a trusted proxy),
|
|
21
|
+
* safe client IP identification cannot be guaranteed, and this function safely falls back to 'unknown'.
|
|
22
|
+
*
|
|
23
|
+
* 2. When `trustProxy` is true:
|
|
24
|
+
* - To prevent forged header injection across reverse proxies (e.g. an attacker sending CF-Connecting-IP
|
|
25
|
+
* to an Nginx server that does not strip it), Z-Shield strictly requires selecting an explicit trusted
|
|
26
|
+
* strategy via `getIp` or providing a custom extraction callback:
|
|
27
|
+
* - 'cloudflare' | 'cf-connecting-ip': Trusts only the CF-Connecting-IP header.
|
|
28
|
+
* - 'nginx' | 'x-real-ip': Trusts only the X-Real-IP header.
|
|
29
|
+
* - 'akamai' | 'true-client-ip': Trusts only the True-Client-IP header.
|
|
30
|
+
* - 'x-forwarded-for': Trusts the first valid IP in X-Forwarded-For (requires outer proxy header stripping).
|
|
31
|
+
* - Custom function: `(req) => string | null | undefined`
|
|
32
|
+
* - If `trustProxy` is true but no extraction strategy or custom function is configured,
|
|
33
|
+
* Z-Shield strictly refuses to guess across foreign proxy headers, and falls back safely to 'unknown'
|
|
34
|
+
* (or socket.remoteAddress if available).
|
|
35
|
+
*
|
|
36
|
+
* @param request - Incoming Web Request or NextLikeRequest
|
|
37
|
+
* @param trustProxy - Whether reverse proxy headers are trusted
|
|
38
|
+
* @param extractorOrStrategy - Trusted IP strategy name or custom extractor function
|
|
39
|
+
* @returns Normalized client IP address or 'unknown'
|
|
40
|
+
*/
|
|
41
|
+
export declare function extractNextClientIp(request: Request | NextLikeRequest, trustProxy: boolean, extractorOrStrategy?: NextClientIpExtractor | NextClientIpStrategy): string;
|
|
42
|
+
/**
|
|
43
|
+
* Factory for creating a Next.js App Router shield instance.
|
|
44
|
+
*
|
|
45
|
+
* Usage:
|
|
46
|
+
* import { createNextShield } from 'z-shield';
|
|
47
|
+
* const shield = createNextShield({ mode: 'protect', maxRequests: 50 });
|
|
48
|
+
*
|
|
49
|
+
* export const GET = shield.protect(async (request) => {
|
|
50
|
+
* return Response.json({ message: 'Hello from protected API' });
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* @param optionsOrInstance - Configuration options or existing ZShieldInstance
|
|
54
|
+
* @returns NextShieldInstance
|
|
55
|
+
*/
|
|
56
|
+
export declare function createNextShield(optionsOrInstance?: NextShieldOptions | ZShieldConfig | ZShieldInstance, adapterOptions?: NextShieldOptions): NextShieldInstance;
|
|
57
|
+
/**
|
|
58
|
+
* Higher-order function wrapping a Next.js App Router route handler with Z-Shield protection.
|
|
59
|
+
*
|
|
60
|
+
* Usage:
|
|
61
|
+
* import { withNextShield } from 'z-shield';
|
|
62
|
+
*
|
|
63
|
+
* export const GET = withNextShield(async (req) => {
|
|
64
|
+
* return Response.json({ ok: true });
|
|
65
|
+
* }, { mode: 'protect', maxRequests: 50 });
|
|
66
|
+
*
|
|
67
|
+
* @param handler - Next.js Route Handler function
|
|
68
|
+
* @param options - Z-Shield configuration options or existing shield instance
|
|
69
|
+
* @returns Wrapped route handler
|
|
70
|
+
*/
|
|
71
|
+
export declare function withNextShield<TContext = unknown>(handler: (request: Request, context?: TContext) => Promise<Response> | Response, options?: NextShieldOptions | ZShieldConfig | NextShieldInstance | ZShieldInstance, adapterOptions?: NextShieldOptions): (request: Request, context?: TContext) => Promise<Response>;
|
|
72
|
+
export type { NextShieldOptions, NextShieldCheckResult, NextShieldInstance, NextRouteHandler, NextClientIpStrategy, NextClientIpExtractor, NextLikeRequest };
|
|
73
|
+
//# sourceMappingURL=next.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,OAAO,KAAK,EACV,aAAa,EAEb,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,SAAS,CAAC;AA6BjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,GAAG,eAAe,EAClC,UAAU,EAAE,OAAO,EACnB,mBAAmB,CAAC,EAAE,qBAAqB,GAAG,oBAAoB,GACjE,MAAM,CA4FR;AA6GD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,aAAa,GAAG,eAAe,EACvE,cAAc,CAAC,EAAE,iBAAiB,GACjC,kBAAkB,CAoPpB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,QAAQ,GAAG,OAAO,EAC/C,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,QAAQ,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,EAC/E,OAAO,CAAC,EAAE,iBAAiB,GAAG,aAAa,GAAG,kBAAkB,GAAG,eAAe,EAClF,cAAc,CAAC,EAAE,iBAAiB,GACjC,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,QAAQ,KAAK,OAAO,CAAC,QAAQ,CAAC,CAU7D;AAED,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EAChB,CAAC"}
|