UNPKG

@lock-dev/payload-guard

Version:

Payload guard detection module for lock.dev security framework

437 lines (424 loc) 14.7 kB
// src/index.ts import { createModule, registerModule } from "@lock-dev/core"; import { LRUCache } from "lru-cache"; // src/types.ts var PayloadGuardEventType = /* @__PURE__ */ ((PayloadGuardEventType2) => { PayloadGuardEventType2["XSS_DETECTED"] = "xss-detected"; PayloadGuardEventType2["SQL_INJECTION_DETECTED"] = "sql-injection-detected"; PayloadGuardEventType2["COMMAND_INJECTION_DETECTED"] = "command-injection-detected"; PayloadGuardEventType2["PATH_TRAVERSAL_DETECTED"] = "path-traversal-detected"; PayloadGuardEventType2["GENERAL_INJECTION_DETECTED"] = "general-injection-detected"; PayloadGuardEventType2["SSRF_DETECTED"] = "ssrf-detected"; return PayloadGuardEventType2; })(PayloadGuardEventType || {}); // src/utils/check.ts function traverseAndCheck(obj, path = "", detectors, excludeFields = []) { if (obj === null || obj === void 0) { return { detected: false }; } if (typeof obj === "string") { const isSafePlainText = (str) => { const hasNoSuspiciousPatterns = !/[<>{}()|;$=\[\]`']/.test(str); const hasSQLKeywordsButNoSyntax = /\b(select|insert|update|delete|from|where)\b/i.test(str) && !/['"]|--|#|\/\*|\b(union|join)\b/i.test(str); return hasNoSuspiciousPatterns || hasSQLKeywordsButNoSyntax; }; if (isSafePlainText(obj)) { return { detected: false }; } for (const detector of detectors) { const result = detector(obj); if (result.detected) { result.path = path; return result; } } return { detected: false }; } if (typeof obj !== "object") { return { detected: false }; } if (Array.isArray(obj)) { for (let i = 0; i < obj.length; i++) { const result = traverseAndCheck( obj[i], path ? `${path}[${i}]` : `[${i}]`, detectors, excludeFields ); if (result.detected) { return result; } } return { detected: false }; } for (const key in obj) { if (excludeFields.includes(key)) { continue; } const newPath = path ? `${path}.${key}` : key; const result = traverseAndCheck(obj[key], newPath, detectors, excludeFields); if (result.detected) { return result; } } return { detected: false }; } // src/patterns/index.ts var PATTERNS = { xss: [ /<script\b[^>]*>[\s\S]*?<\/script\b[^>]*>/i, /<\s*script\s*>|\bon\w+\s*=|\balert\s*\(|javascript\s*:/i, /\bon(?:load|error|mouse|focus|blur|click|change|submit|pointer\w+|animation\w+)\s*=["']/i, /(?:%3C|<)(?:%2F|\/)*(?:%73|s)(?:%63|c)(?:%72|r)(?:%69|i)(?:%70|p)(?:%74|t)/i, /data:(?:text|application)\/(?:javascript|ecmascript|html|xml)/i, /data:.*?;base64,[A-Za-z0-9+/=]+/i, /(?:<[^>]*\s+|\s+)[oO][nN][a-zA-Z]+\s*=|\b(?:eval|setTimeout|setInterval|Function|execScript)\s*\(/i, /<(?:form|input)\b[^>]*\bid\s*=\s*["'](?:parentNode|proto|constructor|test)\b/i, /<(?:form|input)\b[^>]*\bid=[^>]*>(?:\s*<[^>]*\bname\s*=\s*["'](?:innerText|innerHTML|outerHTML)\b)/i, /<(?:svg|img|iframe|form|input|audio|video|link|embed|object)\b[^>]*\b(?:on\w+|src|data|href)\s*=/i, /<\w+[^>]*src\s*=\s*["']https?:\/\/[^"'>]+["']?(?![^>]*>)/i, /(?:javascript|data|vbscript|file):/i ], sqli: [ /(?:\b(?:union\b(?:.{0,20}\bselect\b|\ballselect\b)|select\b.{0,20}\bfrom\b|\bor\b.{0,20}['\d].{0,20}=.{0,20}['\d]|\bdrop\b.{0,20}\btable\b|\bexec\b.{0,20}\bxp_cmdshell\b|\binsert\b.{0,20}\binto\b|\bdelete\b.{0,20}\bfrom\b|\bupdate\b.{0,20}\bset\b)|'(?:\s*(\-\-|\#|\/\*)))/i, /(?:\b(?:--\s|#|\/\*)|\b(?:CONCAT|CHAR|SUBSTRING|ASCII|BENCHMARK|SLEEP|LOAD_FILE|EXTRACTVALUE|UPDATEXML)\s*\()/i, /(?:UNION[\s\/\*]+SELECT|SELECT[\s\/\*]+FROM|AND|OR)[\s\/\*]+\d+=/i, /'\s+(?:AND|OR)\s+(?:'|"|\d+)['"]?\s*(?:=|!=|<>|LIKE)\s*['"]?(?:'|"|\d+)/i, /;\s*(?:CREATE|ALTER|DROP|TRUNCATE|RENAME|INSERT|SELECT|UPDATE|DELETE|MERGE)\s+/i, /\b(?:SEL\s*E*\s*C\s*T|UNI\s*O*\s*N\s+SEL\s*E*\s*C\s*T)\b[\s\n\r\t]*.*?\b(?:FR\s*O*\s*M)\b/i ], commandInjection: [ /(?:\$\(|\`|\|\s*[\w\d\s\-\/\\]+\s*\||\; \w+|\|\|\w+|\&\&\w+|\|\w+)/i, /(?:\/bin\/(?:ba)?sh|cmd(?:\.exe)?|powershell(?:\.exe)?|wget\s|curl\s|nc\s|ncat\s|telnet\s|lftp\s)/i, /\$\(.*?\)|\`.*?\`/i, /(?:;|\||\|\||&&)\s*(?:id|whoami|cat|echo|rm|touch|chmod|chown|wget|curl|bash|sh|python|perl|ruby|php)/i, /\benv\b|\bset\b|\bexport\b|\bPATH=/i, /\u202E|\u202D|\u061C|\u2066|\u2067|\u2068|\u202B|\u202C|\u2069/, /\beval\s*\(|\bFunction\s*\(|\bexec\s*\(|\bsetTimeout\s*\(|\bsetInterval\s*\(/i ], pathTraversal: [ /(?:\.\.\/|\.\.\\|\.\.\%2f|\.\.\%5c|\.\.%252f|\.\.%255c)/i, /%(?:2e|c0%ae|e0%80%ae|c0ae|e0%80ae|25%63%30%61%65)(?:%2e|%c0%ae|%e0%80%ae|%c0ae|%e0%80ae|%25%63%30%61%65)/i, /(?:\/etc\/passwd|\/etc\/shadow|\/etc\/hosts|boot\.ini|win\.ini|\/proc\/self\/environ)/i, /(?:\/\.\.\/|\\\.\.\\|%5c\.\.%5c|%2f\.\.%2f)/i, /(?:%00|%0a|%0d)/i, /https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|10\.\d+\.\d+\.\d+|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+)(?::\d+)?/i, /https?:\/\/(?:[^.]+\.)?(?:internal|corp|local|intranet|private|localhost)(?:$|\/|:)/i ], nosql: [ /\$(?:ne|gt|lt|gte|lte|in|nin|not|or|and|regex|where|elemMatch|exists|type|mod|all|size|within|slice|max|min)/i, /\{\s*\$(?:gt|lt|gte|lte|ne|in|nin|not|or|and|regex|where)\s*:/i, /\$(?:function|eval|where)\s*:/i, /["']__proto__["']|["']constructor["']|["']prototype["']/i, /^\s*\{\s*\$[a-z]+:/i ], templateInjection: [ /\{\{\s*[\w\._\[\]\(\)]+\s*\}\}/i, /#\{.+?\}|\${.+?}|\$\{.+?\}|\<\%.+?\%\>/i, /\$\{[\w\._\[\]\(\)\'\"]+\}/i, /\{\{.*(?:constructor|prototype|window|document|eval|alert|confirm).*\}\}/i, /\{\{.*(?:constructor\.constructor|__proto__|Object\.|Function\.|eval\().*\}\}/i ] }; // src/utils/command-injection.ts function detectCommandInjection(value) { if (typeof value !== "string") return { detected: false }; for (const pattern of PATTERNS.commandInjection) { if (pattern.test(value)) { return { detected: true, type: "command-injection-detected" /* COMMAND_INJECTION_DETECTED */, value, pattern }; } } return { detected: false }; } // src/utils/hash.ts function generateHash(str) { let hash = 0; if (str.length === 0) return hash.toString(); for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; } return hash.toString(); } // src/utils/no-sqli.ts function detectNoSQLi(value) { if (typeof value !== "string") return { detected: false }; for (const pattern of PATTERNS.nosql) { if (pattern.test(value)) { return { detected: true, type: "general-injection-detected" /* GENERAL_INJECTION_DETECTED */, value, pattern }; } } return { detected: false }; } // src/utils/path-traversal.ts function detectPathTraversal(value) { if (typeof value !== "string") return { detected: false }; for (const pattern of PATTERNS.pathTraversal) { if (pattern.test(value)) { return { detected: true, type: "path-traversal-detected" /* PATH_TRAVERSAL_DETECTED */, value, pattern }; } } return { detected: false }; } // src/utils/sqli.ts function detectSQLi(value) { if (typeof value !== "string") return { detected: false }; for (const pattern of PATTERNS.sqli) { if (pattern.test(value)) { return { detected: true, type: "sql-injection-detected" /* SQL_INJECTION_DETECTED */, value, pattern }; } } return { detected: false }; } // src/utils/ssrf.ts function detectSSRF(value) { if (typeof value !== "string") return { detected: false }; if (value.includes("localhost") || value.includes("127.0.0.1") || value.match(/192\.168\.\d+\.\d+/) || value.match(/10\.\d+\.\d+\.\d+/) || value.match(/172\.(1[6-9]|2\d|3[01])\.\d+\.\d+/)) { return { detected: true, type: "path-traversal-detected" /* PATH_TRAVERSAL_DETECTED */, value, pattern: /localhost|127\.0\.0\.1/ }; } const ssrfPatterns = [ /https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0)/i, /https?:\/\/(?:10\.\d+\.\d+\.\d+|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+)/i, /https?:\/\/(?:[^.]+\.)?(?:internal|corp|local|intranet|private)/i, /https?:\/\/[^\/]+(?:\.\/|\.\.\/|%2e\.\/|%2e%2e\/)/i, /^file:\/\//i ]; for (const pattern of ssrfPatterns) { if (pattern.test(value)) { return { detected: true, type: "path-traversal-detected" /* PATH_TRAVERSAL_DETECTED */, value, pattern }; } } return { detected: false }; } // src/utils/template-injection.ts function detectTemplateInjection(value) { if (typeof value !== "string") return { detected: false }; for (const pattern of PATTERNS.templateInjection) { if (pattern.test(value)) { return { detected: true, type: "general-injection-detected" /* GENERAL_INJECTION_DETECTED */, value, pattern }; } } return { detected: false }; } // src/utils/xss.ts function detectXSS(value) { if (typeof value !== "string") return { detected: false }; if (/<form\s+id=test>.*?<input\s+id=parentNode\s+name=innerText>/i.test(value)) { return { detected: true, type: "xss-detected" /* XSS_DETECTED */, value, pattern: /<form\s+id=test>.*?<input\s+id=parentNode\s+name=innerText>/i }; } for (const pattern of PATTERNS.xss) { if (pattern.test(value)) { return { detected: true, type: "xss-detected" /* XSS_DETECTED */, value, pattern }; } } return { detected: false }; } // src/index.ts var DEFAULT_CONFIG = { mode: "block", blockStatusCode: 403, blockMessage: "Request blocked due to potential security threat", checkParts: ["params", "query", "body", "headers"], excludeHeaders: ["authorization", "cookie", "set-cookie"], excludeFields: [], detectXSS: true, detectSSRF: true, detectSQLi: true, detectCommandInjection: true, detectPathTraversal: true, enableCaching: true, cacheTtl: 36e5, cacheSize: 1e4, failBehavior: "open" }; var payloadGuard = createModule({ name: "payload-guard", defaultConfig: DEFAULT_CONFIG, async check(context, config) { try { const cache = config.enableCaching ? new LRUCache({ max: config.cacheSize, ttl: config.cacheTtl, ttlAutopurge: true }) : null; const detectors = []; if (config.detectXSS) { detectors.push(detectXSS); } if (config.detectSQLi) { detectors.push(detectSQLi); } if (config.detectSSRF) { detectors.push(detectSSRF); } if (config.detectCommandInjection) { detectors.push(detectCommandInjection); } if (config.detectPathTraversal) { detectors.push(detectPathTraversal); } detectors.push(detectNoSQLi); detectors.push(detectTemplateInjection); if (detectors.length === 0) { return { passed: true }; } const checkParts = config.checkParts || ["params", "query", "body", "headers"]; const excludeHeaders = (config.excludeHeaders || []).map((h) => h.toLowerCase()); const req = context.request; for (const part of checkParts) { if (!req[part]) continue; if (part === "headers") { const headers = { ...req.headers }; for (const header of excludeHeaders) { delete headers[header]; } const result = traverseAndCheck( headers, "headers", detectors, config.excludeFields || [] ); if (result.detected) { return { passed: config.mode === "detect", reason: result.type, data: { path: result.path, value: result.value, pattern: result.pattern?.toString() }, severity: "high" }; } } else { let result; if (cache) { const reqPartStr = JSON.stringify(req[part]); const hash = generateHash(reqPartStr); const cachedResult = cache.get(hash); if (cachedResult) { result = cachedResult; } else { result = traverseAndCheck(req[part], part, detectors, config.excludeFields || []); cache.set(hash, result); } } else { result = traverseAndCheck(req[part], part, detectors, config.excludeFields || []); } if (result.detected) { return { passed: config.mode === "detect", reason: result.type, data: { path: result.path, value: result.value, pattern: result.pattern?.toString() }, severity: "high" }; } } } return { passed: true }; } catch (error) { console.error(`Unexpected error in PayloadGuard module:`, error); return { passed: config.failBehavior !== "closed", reason: config.failBehavior === "closed" ? "general-injection-detected" /* GENERAL_INJECTION_DETECTED */ : void 0, data: config.failBehavior === "closed" ? { error: "PayloadGuard module failed" } : void 0, severity: "medium" }; } }, async handleFailure(context, reason, data) { const config = context.data.get("payload-guard:config"); const res = context.response; if (res.headersSent || res.writableEnded) { return; } const message = config.blockMessage || "Request blocked due to potential security threat"; const statusCode = config.blockStatusCode || 403; if (typeof res.status === "function") { return res.status(statusCode).json({ error: message, details: { reason, ...data } }); } else if (typeof res.statusCode === "number") { res.statusCode = statusCode; res.setHeader("Content-Type", "application/json"); return res.end( JSON.stringify({ error: message, details: { reason, ...data } }) ); } } }); registerModule("payloadGuard", payloadGuard); export { PATTERNS, PayloadGuardEventType, detectCommandInjection, detectNoSQLi, detectPathTraversal, detectSQLi, detectSSRF, detectTemplateInjection, detectXSS, generateHash, payloadGuard, traverseAndCheck };