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