Files
git.stella-ops.org/tools/stella-callgraph-node/sink-detect.js
StellaOps Bot 5146204f1b 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.
2025-12-22 23:21:21 +02:00

231 lines
8.3 KiB
JavaScript

// -----------------------------------------------------------------------------
// 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));
}