feat: add security sink detection patterns for JavaScript/TypeScript
- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations). - Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns. - Added `package-lock.json` for dependency management.
This commit is contained in:
@@ -9,6 +9,10 @@ import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
||||
import { join, extname, relative, dirname } from 'path';
|
||||
import { parse } from '@babel/parser';
|
||||
import traverse from '@babel/traverse';
|
||||
import { buildSinkLookup, matchSink } from './sink-detect.js';
|
||||
|
||||
// Pre-build sink lookup for fast detection
|
||||
const sinkLookup = buildSinkLookup();
|
||||
|
||||
/**
|
||||
* Main entry point
|
||||
@@ -72,16 +76,18 @@ async function analyzeProject(projectPath) {
|
||||
const nodes = [];
|
||||
const edges = [];
|
||||
const entrypoints = [];
|
||||
const sinks = [];
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
try {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
const relativePath = relative(projectPath, file);
|
||||
const result = analyzeFile(content, relativePath, packageInfo.name);
|
||||
|
||||
|
||||
nodes.push(...result.nodes);
|
||||
edges.push(...result.edges);
|
||||
entrypoints.push(...result.entrypoints);
|
||||
sinks.push(...result.sinks);
|
||||
} catch (error) {
|
||||
// Skip files that can't be parsed
|
||||
console.error(`Warning: Could not parse ${file}: ${error.message}`);
|
||||
@@ -93,7 +99,8 @@ async function analyzeProject(projectPath) {
|
||||
version: packageInfo.version,
|
||||
nodes: deduplicateNodes(nodes),
|
||||
edges: deduplicateEdges(edges),
|
||||
entrypoints
|
||||
entrypoints,
|
||||
sinks: deduplicateSinks(sinks)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -142,6 +149,7 @@ function analyzeFile(content, relativePath, packageName) {
|
||||
const nodes = [];
|
||||
const edges = [];
|
||||
const entrypoints = [];
|
||||
const sinks = [];
|
||||
const moduleBase = relativePath.replace(/\.[^.]+$/, '').replace(/\\/g, '/');
|
||||
|
||||
// Parse with Babel
|
||||
@@ -273,13 +281,16 @@ function analyzeFile(content, relativePath, packageName) {
|
||||
|
||||
const callee = path.node.callee;
|
||||
let targetId = null;
|
||||
let objName = null;
|
||||
let methodName = null;
|
||||
|
||||
if (callee.type === 'Identifier') {
|
||||
targetId = `js:${packageName}/${moduleBase}.${callee.name}`;
|
||||
methodName = callee.name;
|
||||
} else if (callee.type === 'MemberExpression') {
|
||||
const objName = callee.object?.name || 'unknown';
|
||||
const propName = callee.property?.name || 'unknown';
|
||||
targetId = `js:external/${objName}.${propName}`;
|
||||
objName = callee.object?.name || 'unknown';
|
||||
methodName = callee.property?.name || 'unknown';
|
||||
targetId = `js:external/${objName}.${methodName}`;
|
||||
}
|
||||
|
||||
if (targetId) {
|
||||
@@ -294,12 +305,29 @@ function analyzeFile(content, relativePath, packageName) {
|
||||
});
|
||||
}
|
||||
|
||||
// Detect security sinks
|
||||
if (methodName) {
|
||||
const sinkMatch = matchSink(objName || methodName, methodName, sinkLookup);
|
||||
if (sinkMatch) {
|
||||
sinks.push({
|
||||
caller: currentFunction,
|
||||
category: sinkMatch.category,
|
||||
method: `${objName ? objName + '.' : ''}${methodName}`,
|
||||
site: {
|
||||
file: relativePath,
|
||||
line: path.node.loc?.start.line || 0,
|
||||
column: path.node.loc?.start.column || 0
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Detect Express/Fastify route registration
|
||||
detectRouteRegistration(path, entrypoints, packageName, moduleBase, relativePath);
|
||||
}
|
||||
});
|
||||
|
||||
return { nodes, edges, entrypoints };
|
||||
return { nodes, edges, entrypoints, sinks };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -418,7 +446,7 @@ function deduplicateNodes(nodes) {
|
||||
|
||||
/**
|
||||
* Remove duplicate edges
|
||||
* @param {any[]} edges
|
||||
* @param {any[]} edges
|
||||
* @returns {any[]}
|
||||
*/
|
||||
function deduplicateEdges(edges) {
|
||||
@@ -431,5 +459,20 @@ function deduplicateEdges(edges) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove duplicate sinks
|
||||
* @param {any[]} sinks
|
||||
* @returns {any[]}
|
||||
*/
|
||||
function deduplicateSinks(sinks) {
|
||||
const seen = new Set();
|
||||
return sinks.filter(s => {
|
||||
const key = `${s.caller}|${s.category}|${s.method}|${s.site.file}:${s.site.line}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Run
|
||||
main().catch(console.error);
|
||||
|
||||
243
tools/stella-callgraph-node/package-lock.json
generated
Normal file
243
tools/stella-callgraph-node/package-lock.json
generated
Normal file
@@ -0,0 +1,243 @@
|
||||
{
|
||||
"name": "stella-callgraph-node",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "stella-callgraph-node",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@babel/traverse": "^7.23.0",
|
||||
"@babel/types": "^7.23.0"
|
||||
},
|
||||
"bin": {
|
||||
"stella-callgraph-node": "index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
|
||||
"integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@babel/types": "^7.28.5",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-globals": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
||||
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
|
||||
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.5"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/parser": "^7.27.2",
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
|
||||
"integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.5",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
|
||||
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.27",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
|
||||
"integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
230
tools/stella-callgraph-node/sink-detect.js
Normal file
230
tools/stella-callgraph-node/sink-detect.js
Normal file
@@ -0,0 +1,230 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// sink-detect.js
|
||||
// Security sink detection patterns for JavaScript/TypeScript.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sink detection patterns organized by category.
|
||||
*/
|
||||
export const sinkPatterns = {
|
||||
command_injection: {
|
||||
category: 'command_injection',
|
||||
patterns: [
|
||||
{ module: 'child_process', methods: ['exec', 'execSync', 'spawn', 'spawnSync', 'execFile', 'execFileSync', 'fork'] },
|
||||
{ module: 'shelljs', methods: ['exec', 'which', 'cat', 'sed', 'grep', 'rm', 'cp', 'mv', 'mkdir'] },
|
||||
{ object: 'process', methods: ['exec'] }
|
||||
]
|
||||
},
|
||||
|
||||
sql_injection: {
|
||||
category: 'sql_injection',
|
||||
patterns: [
|
||||
{ object: 'connection', methods: ['query', 'execute'] },
|
||||
{ object: 'pool', methods: ['query', 'execute'] },
|
||||
{ object: 'client', methods: ['query'] },
|
||||
{ module: 'mysql', methods: ['query', 'execute'] },
|
||||
{ module: 'mysql2', methods: ['query', 'execute'] },
|
||||
{ module: 'pg', methods: ['query'] },
|
||||
{ module: 'sqlite3', methods: ['run', 'exec', 'all', 'get'] },
|
||||
{ module: 'knex', methods: ['raw', 'whereRaw', 'havingRaw', 'orderByRaw'] },
|
||||
{ module: 'sequelize', methods: ['query', 'literal'] },
|
||||
{ module: 'typeorm', methods: ['query', 'createQueryBuilder'] },
|
||||
{ module: 'prisma', methods: ['$queryRaw', '$executeRaw', '$queryRawUnsafe', '$executeRawUnsafe'] }
|
||||
]
|
||||
},
|
||||
|
||||
file_write: {
|
||||
category: 'file_write',
|
||||
patterns: [
|
||||
{ module: 'fs', methods: ['writeFile', 'writeFileSync', 'appendFile', 'appendFileSync', 'createWriteStream', 'rename', 'renameSync', 'unlink', 'unlinkSync', 'rmdir', 'rmdirSync', 'rm', 'rmSync'] },
|
||||
{ module: 'fs/promises', methods: ['writeFile', 'appendFile', 'rename', 'unlink', 'rmdir', 'rm'] }
|
||||
]
|
||||
},
|
||||
|
||||
file_read: {
|
||||
category: 'file_read',
|
||||
patterns: [
|
||||
{ module: 'fs', methods: ['readFile', 'readFileSync', 'createReadStream', 'readdir', 'readdirSync'] },
|
||||
{ module: 'fs/promises', methods: ['readFile', 'readdir'] }
|
||||
]
|
||||
},
|
||||
|
||||
deserialization: {
|
||||
category: 'deserialization',
|
||||
patterns: [
|
||||
{ global: true, methods: ['eval', 'Function'] },
|
||||
{ object: 'JSON', methods: ['parse'] },
|
||||
{ module: 'vm', methods: ['runInContext', 'runInNewContext', 'runInThisContext', 'createScript'] },
|
||||
{ module: 'serialize-javascript', methods: ['deserialize'] },
|
||||
{ module: 'node-serialize', methods: ['unserialize'] },
|
||||
{ module: 'js-yaml', methods: ['load', 'loadAll'] }
|
||||
]
|
||||
},
|
||||
|
||||
ssrf: {
|
||||
category: 'ssrf',
|
||||
patterns: [
|
||||
{ module: 'http', methods: ['request', 'get'] },
|
||||
{ module: 'https', methods: ['request', 'get'] },
|
||||
{ module: 'axios', methods: ['get', 'post', 'put', 'delete', 'patch', 'request'] },
|
||||
{ module: 'node-fetch', methods: ['default'] },
|
||||
{ global: true, methods: ['fetch'] },
|
||||
{ module: 'got', methods: ['get', 'post', 'put', 'delete', 'patch'] },
|
||||
{ module: 'superagent', methods: ['get', 'post', 'put', 'delete', 'patch'] },
|
||||
{ module: 'request', methods: ['get', 'post', 'put', 'delete', 'patch'] },
|
||||
{ module: 'undici', methods: ['request', 'fetch'] }
|
||||
]
|
||||
},
|
||||
|
||||
path_traversal: {
|
||||
category: 'path_traversal',
|
||||
patterns: [
|
||||
{ module: 'path', methods: ['join', 'resolve', 'normalize'] },
|
||||
{ module: 'fs', methods: ['readFile', 'readFileSync', 'writeFile', 'writeFileSync', 'access', 'accessSync', 'stat', 'statSync'] }
|
||||
]
|
||||
},
|
||||
|
||||
weak_crypto: {
|
||||
category: 'weak_crypto',
|
||||
patterns: [
|
||||
{ module: 'crypto', methods: ['createCipher', 'createDecipher', 'createCipheriv', 'createDecipheriv'] },
|
||||
{ object: 'crypto', methods: ['createHash'] } // MD5, SHA1 are weak
|
||||
]
|
||||
},
|
||||
|
||||
ldap_injection: {
|
||||
category: 'ldap_injection',
|
||||
patterns: [
|
||||
{ module: 'ldapjs', methods: ['search', 'modify', 'add', 'del'] },
|
||||
{ module: 'activedirectory', methods: ['find', 'findUser', 'findGroup'] }
|
||||
]
|
||||
},
|
||||
|
||||
nosql_injection: {
|
||||
category: 'nosql_injection',
|
||||
patterns: [
|
||||
{ module: 'mongodb', methods: ['find', 'findOne', 'updateOne', 'updateMany', 'deleteOne', 'deleteMany', 'aggregate'] },
|
||||
{ module: 'mongoose', methods: ['find', 'findOne', 'findById', 'updateOne', 'updateMany', 'deleteOne', 'deleteMany', 'where', 'aggregate'] }
|
||||
]
|
||||
},
|
||||
|
||||
xss: {
|
||||
category: 'xss',
|
||||
patterns: [
|
||||
{ object: 'document', methods: ['write', 'writeln'] },
|
||||
{ object: 'element', methods: ['innerHTML', 'outerHTML'] },
|
||||
{ module: 'dangerouslySetInnerHTML', methods: ['__html'] } // React pattern
|
||||
]
|
||||
},
|
||||
|
||||
log_injection: {
|
||||
category: 'log_injection',
|
||||
patterns: [
|
||||
{ object: 'console', methods: ['log', 'info', 'warn', 'error', 'debug'] },
|
||||
{ module: 'winston', methods: ['log', 'info', 'warn', 'error', 'debug'] },
|
||||
{ module: 'pino', methods: ['info', 'warn', 'error', 'debug', 'trace'] },
|
||||
{ module: 'bunyan', methods: ['info', 'warn', 'error', 'debug', 'trace'] }
|
||||
]
|
||||
},
|
||||
|
||||
regex_dos: {
|
||||
category: 'regex_dos',
|
||||
patterns: [
|
||||
{ object: 'RegExp', methods: ['test', 'exec', 'match'] },
|
||||
{ global: true, methods: ['RegExp'] }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a lookup map for fast sink detection.
|
||||
* @returns {Map<string, { category: string, method: string }>}
|
||||
*/
|
||||
export function buildSinkLookup() {
|
||||
const lookup = new Map();
|
||||
|
||||
for (const [_, config] of Object.entries(sinkPatterns)) {
|
||||
for (const pattern of config.patterns) {
|
||||
for (const method of pattern.methods) {
|
||||
// Key formats: "module:method", "object.method", "global:method"
|
||||
if (pattern.module) {
|
||||
lookup.set(`${pattern.module}:${method}`, { category: config.category, method });
|
||||
}
|
||||
if (pattern.object) {
|
||||
lookup.set(`${pattern.object}.${method}`, { category: config.category, method });
|
||||
}
|
||||
if (pattern.global) {
|
||||
lookup.set(`global:${method}`, { category: config.category, method });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a call expression is a security sink.
|
||||
* @param {string} objectOrModule - The object/module name (e.g., 'fs', 'child_process', 'connection')
|
||||
* @param {string} methodName - The method being called
|
||||
* @param {Map} sinkLookup - Pre-built sink lookup map
|
||||
* @returns {{ category: string, method: string } | null}
|
||||
*/
|
||||
export function matchSink(objectOrModule, methodName, sinkLookup) {
|
||||
// Check module:method pattern
|
||||
const moduleKey = `${objectOrModule}:${methodName}`;
|
||||
if (sinkLookup.has(moduleKey)) {
|
||||
return sinkLookup.get(moduleKey);
|
||||
}
|
||||
|
||||
// Check object.method pattern
|
||||
const objectKey = `${objectOrModule}.${methodName}`;
|
||||
if (sinkLookup.has(objectKey)) {
|
||||
return sinkLookup.get(objectKey);
|
||||
}
|
||||
|
||||
// Check global functions
|
||||
const globalKey = `global:${objectOrModule}`;
|
||||
if (sinkLookup.has(globalKey)) {
|
||||
return sinkLookup.get(globalKey);
|
||||
}
|
||||
|
||||
// Check if methodName itself is a global sink (like eval)
|
||||
const directGlobal = `global:${methodName}`;
|
||||
if (sinkLookup.has(directGlobal)) {
|
||||
return sinkLookup.get(directGlobal);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common dangerous patterns that indicate direct user input flow.
|
||||
*/
|
||||
export const taintSources = [
|
||||
'req.body',
|
||||
'req.query',
|
||||
'req.params',
|
||||
'req.headers',
|
||||
'req.cookies',
|
||||
'request.body',
|
||||
'request.query',
|
||||
'request.params',
|
||||
'event.body',
|
||||
'event.queryStringParameters',
|
||||
'event.pathParameters',
|
||||
'ctx.request.body',
|
||||
'ctx.request.query',
|
||||
'ctx.params',
|
||||
'process.env',
|
||||
'process.argv'
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if an identifier is a potential taint source.
|
||||
* @param {string} identifier
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isTaintSource(identifier) {
|
||||
return taintSources.some(source => identifier.includes(source));
|
||||
}
|
||||
Reference in New Issue
Block a user