@lock-dev/payload-guard
Version:
Payload guard detection module for lock.dev security framework
437 lines (424 loc) • 14.7 kB
JavaScript
// 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
};