mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-21 20:18:06 +02:00
Phase 1 (#33)
* chore: Exclude CLAUDE.md from Cargo.toml * feat: add callgraph module and integrate into main analysis flow * feat: enhance CLI with new severity filtering and analysis modes * feat: update CHANGELOG with recent enhancements and fixes to severity filtering and output handling * feat: implement state-model dataflow analysis for resource lifecycle and auth state * feat: enhance diagnostic output formatting and add evidence structure * feat: implement attack surface ranking for diagnostics with scoring and sorting * feat: add comprehensive documentation for installation, usage, and rules reference * feat: add multiple language support for command execution and evaluation endpoints * feat: implement inline suppression for findings using `nyx:ignore` comments * feat: add confidence levels to AST patterns and update output structure * feat: implement low-noise prioritization system with category filtering, rollup grouping, and configurable budgets * feat: bump version to 0.4.0 and update changelog with new features and improvements * feat: add dead code allowances to various functions in mod.rs and real_world_tests.rs
This commit is contained in:
parent
19b578c5c4
commit
1bbe4b1cfb
456 changed files with 25628 additions and 1228 deletions
47
tests/fixtures/real_world/javascript/cfg/async_await_flow.expect.json
vendored
Normal file
47
tests/fixtures/real_world/javascript/cfg/async_await_flow.expect.json
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"description": "Async/await control flow with promisified exec and fetch-to-exec pipeline. Tests CFG handling of async patterns.",
|
||||
"tags": [
|
||||
"cfg",
|
||||
"async",
|
||||
"cmdi",
|
||||
"fetch"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
5,
|
||||
9
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "execAsync is a promisified wrapper; scanner cannot trace through util.promisify to recognize it as a sink"
|
||||
},
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
13,
|
||||
19
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "fetch response flows to child_process.exec but fetch is not a recognized source"
|
||||
},
|
||||
{
|
||||
"rule_id": "cfg-unguarded-sink",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
15,
|
||||
19
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "child_process.exec called without validation of data.command"
|
||||
}
|
||||
]
|
||||
}
|
||||
20
tests/fixtures/real_world/javascript/cfg/async_await_flow.js
vendored
Normal file
20
tests/fixtures/real_world/javascript/cfg/async_await_flow.js
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
var child_process = require('child_process');
|
||||
var util = require('util');
|
||||
var execAsync = util.promisify(child_process.exec);
|
||||
|
||||
async function runCommand(userCmd) {
|
||||
try {
|
||||
var result = await execAsync(userCmd);
|
||||
return result.stdout;
|
||||
} catch (err) {
|
||||
return err.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAndExec(url) {
|
||||
var response = await fetch(url);
|
||||
var data = await response.json();
|
||||
child_process.exec(data.command, function(err, stdout) {
|
||||
return stdout;
|
||||
});
|
||||
}
|
||||
36
tests/fixtures/real_world/javascript/cfg/callback_nesting.expect.json
vendored
Normal file
36
tests/fixtures/real_world/javascript/cfg/callback_nesting.expect.json
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"description": "Nested callbacks where fs.readFile data flows into child_process.exec. Tests CFG handling of callback nesting and data flow across callback boundaries.",
|
||||
"tags": [
|
||||
"cfg",
|
||||
"callbacks",
|
||||
"cmdi",
|
||||
"fs"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
3,
|
||||
11
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "Data read from fs.readFile flows into child_process.exec but data is a callback param, not a recognized taint source"
|
||||
},
|
||||
{
|
||||
"rule_id": "cfg-unguarded-sink",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
7,
|
||||
11
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "child_process.exec receives unchecked file contents"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
tests/fixtures/real_world/javascript/cfg/callback_nesting.js
vendored
Normal file
13
tests/fixtures/real_world/javascript/cfg/callback_nesting.js
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
var child_process = require('child_process');
|
||||
var fs = require('fs');
|
||||
|
||||
function processInput(input, callback) {
|
||||
fs.readFile(input.path, 'utf8', function(err, data) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
child_process.exec(data, function(execErr, stdout) {
|
||||
callback(execErr, stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
58
tests/fixtures/real_world/javascript/cfg/switch_fallthrough.expect.json
vendored
Normal file
58
tests/fixtures/real_world/javascript/cfg/switch_fallthrough.expect.json
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"description": "Switch statement with fallthrough from exec case to safe case (missing break). eval and execSync with function parameter.",
|
||||
"tags": [
|
||||
"cfg",
|
||||
"switch",
|
||||
"fallthrough",
|
||||
"code-exec"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "js.code_exec.eval",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
4,
|
||||
8
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "AST pattern matches eval() call"
|
||||
},
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
4,
|
||||
8
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "userInput is a function param, not a recognized source; requires interprocedural analysis"
|
||||
},
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
10,
|
||||
14
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "userInput flows to child_process.execSync but userInput is a param, not a recognized source"
|
||||
},
|
||||
{
|
||||
"rule_id": "cfg-error-fallthrough",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
9,
|
||||
16
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "Missing break after exec case causes fallthrough to safe case"
|
||||
}
|
||||
]
|
||||
}
|
||||
19
tests/fixtures/real_world/javascript/cfg/switch_fallthrough.js
vendored
Normal file
19
tests/fixtures/real_world/javascript/cfg/switch_fallthrough.js
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
var child_process = require('child_process');
|
||||
|
||||
function handleAction(action, userInput) {
|
||||
switch (action) {
|
||||
case 'eval':
|
||||
eval(userInput);
|
||||
break;
|
||||
case 'log':
|
||||
console.log(userInput);
|
||||
break;
|
||||
case 'exec':
|
||||
child_process.execSync(userInput);
|
||||
case 'safe':
|
||||
console.log('safe action');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
25
tests/fixtures/real_world/javascript/cfg/try_catch_finally.expect.json
vendored
Normal file
25
tests/fixtures/real_world/javascript/cfg/try_catch_finally.expect.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"description": "Try-catch-finally resource handling. processFile properly closes fd in finally block. leakyProcess leaks fd when throw occurs before closeSync.",
|
||||
"tags": [
|
||||
"cfg",
|
||||
"resource-leak",
|
||||
"try-catch",
|
||||
"fs"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "cfg-resource-leak",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
17,
|
||||
25
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "fd leaks when throw fires at line 22 before closeSync at line 24; scanner may not track fd lifecycle in JS"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
tests/fixtures/real_world/javascript/cfg/try_catch_finally.js
vendored
Normal file
26
tests/fixtures/real_world/javascript/cfg/try_catch_finally.js
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
var fs = require('fs');
|
||||
|
||||
function processFile(path) {
|
||||
var fd;
|
||||
try {
|
||||
fd = fs.openSync(path, 'r');
|
||||
var data = fs.readFileSync(fd, 'utf8');
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
if (fd !== undefined) {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function leakyProcess(path) {
|
||||
var fd = fs.openSync(path, 'r');
|
||||
var data = fs.readFileSync(fd, 'utf8');
|
||||
if (data.length === 0) {
|
||||
throw new Error('empty');
|
||||
}
|
||||
fs.closeSync(fd);
|
||||
return data;
|
||||
}
|
||||
60
tests/fixtures/real_world/javascript/mixed/express_auth_cmdi.expect.json
vendored
Normal file
60
tests/fixtures/real_world/javascript/mixed/express_auth_cmdi.expect.json
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"description": "Express route with command injection: one without auth check, one with auth check. Taint flows in both cases since auth does not sanitize the input.",
|
||||
"tags": [
|
||||
"mixed",
|
||||
"taint",
|
||||
"cfg",
|
||||
"cmdi",
|
||||
"auth",
|
||||
"express"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
9,
|
||||
14
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "req.query.branch flows into child_process.exec in unauthed /deploy route"
|
||||
},
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
21,
|
||||
26
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "req.query.branch flows into child_process.exec in authed /deploy-safe route; auth check does not sanitize"
|
||||
},
|
||||
{
|
||||
"rule_id": "cfg-auth-gap",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
8,
|
||||
17
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "No auth check before child_process.exec in /deploy handler"
|
||||
},
|
||||
{
|
||||
"rule_id": "cfg-unguarded-sink",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
10,
|
||||
14
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "child_process.exec called without input validation guard"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
tests/fixtures/real_world/javascript/mixed/express_auth_cmdi.js
vendored
Normal file
26
tests/fixtures/real_world/javascript/mixed/express_auth_cmdi.js
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
var express = require('express');
|
||||
var child_process = require('child_process');
|
||||
var app = express();
|
||||
|
||||
function isAdmin(req) {
|
||||
return req.headers['x-admin'] === 'true';
|
||||
}
|
||||
|
||||
// Missing auth check before dangerous operation
|
||||
app.get('/deploy', function(req, res) {
|
||||
var branch = req.query.branch;
|
||||
child_process.exec('git checkout ' + branch, function(err, stdout) {
|
||||
res.send(stdout);
|
||||
});
|
||||
});
|
||||
|
||||
// Has auth check but taint still flows
|
||||
app.get('/deploy-safe', function(req, res) {
|
||||
if (!isAdmin(req)) {
|
||||
return res.status(403).send('Forbidden');
|
||||
}
|
||||
var branch = req.query.branch;
|
||||
child_process.exec('git checkout ' + branch, function(err, stdout) {
|
||||
res.send(stdout);
|
||||
});
|
||||
});
|
||||
49
tests/fixtures/real_world/javascript/mixed/taint_through_state.expect.json
vendored
Normal file
49
tests/fixtures/real_world/javascript/mixed/taint_through_state.expect.json
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"description": "Combined taint and state: req.body.name used in file path, fd leaks on early return, and a separate command injection via req.query.cmd.",
|
||||
"tags": [
|
||||
"mixed",
|
||||
"taint",
|
||||
"state",
|
||||
"resource-leak",
|
||||
"cmdi",
|
||||
"express"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
18,
|
||||
23
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "req.query.cmd flows directly into child_process.exec"
|
||||
},
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
5,
|
||||
11
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "req.body.name flows into fs.openSync path but fs.openSync is not a recognized taint sink"
|
||||
},
|
||||
{
|
||||
"rule_id": "state-resource-leak-possible",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
7,
|
||||
15
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "fd from fs.openSync leaks when early return fires at line 13; scanner may not track JS fd lifecycle"
|
||||
}
|
||||
]
|
||||
}
|
||||
24
tests/fixtures/real_world/javascript/mixed/taint_through_state.js
vendored
Normal file
24
tests/fixtures/real_world/javascript/mixed/taint_through_state.js
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
var express = require('express');
|
||||
var fs = require('fs');
|
||||
var child_process = require('child_process');
|
||||
var app = express();
|
||||
|
||||
app.post('/upload', function(req, res) {
|
||||
var filename = req.body.name;
|
||||
var content = req.body.data;
|
||||
var fd = fs.openSync('/tmp/' + filename, 'w');
|
||||
fs.writeSync(fd, content);
|
||||
// fd leaks on early return
|
||||
if (content.length > 1000000) {
|
||||
return res.status(413).send('Too large');
|
||||
}
|
||||
fs.closeSync(fd);
|
||||
res.send('OK');
|
||||
});
|
||||
|
||||
app.get('/run', function(req, res) {
|
||||
var cmd = req.query.cmd;
|
||||
child_process.exec(cmd, function(err, stdout) {
|
||||
res.send(stdout);
|
||||
});
|
||||
});
|
||||
25
tests/fixtures/real_world/javascript/state/db_connection.expect.json
vendored
Normal file
25
tests/fixtures/real_world/javascript/state/db_connection.expect.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"description": "Database connection leak: queryUnsafe creates a mysql connection without calling conn.end(). querySafe properly closes.",
|
||||
"tags": [
|
||||
"state",
|
||||
"resource-leak",
|
||||
"database",
|
||||
"mysql"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "state-resource-leak",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
2,
|
||||
11
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "mysql.createConnection without conn.end(); scanner may not track mysql connection lifecycle"
|
||||
}
|
||||
]
|
||||
}
|
||||
19
tests/fixtures/real_world/javascript/state/db_connection.js
vendored
Normal file
19
tests/fixtures/real_world/javascript/state/db_connection.js
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
var mysql = require('mysql');
|
||||
|
||||
function queryUnsafe() {
|
||||
var conn = mysql.createConnection({ host: 'localhost' });
|
||||
conn.connect();
|
||||
conn.query('SELECT 1', function(err, results) {
|
||||
console.log(results);
|
||||
});
|
||||
// Missing conn.end()
|
||||
}
|
||||
|
||||
function querySafe() {
|
||||
var conn = mysql.createConnection({ host: 'localhost' });
|
||||
conn.connect();
|
||||
conn.query('SELECT 1', function(err, results) {
|
||||
console.log(results);
|
||||
conn.end();
|
||||
});
|
||||
}
|
||||
25
tests/fixtures/real_world/javascript/state/event_listener_leak.expect.json
vendored
Normal file
25
tests/fixtures/real_world/javascript/state/event_listener_leak.expect.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"description": "Event listener leak: socket connections accumulate without cleanup handlers. Safe version registers close and error handlers.",
|
||||
"tags": [
|
||||
"state",
|
||||
"resource-leak",
|
||||
"event-listener",
|
||||
"net"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "state-resource-leak",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
3,
|
||||
14
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "Socket connections accumulate in array without close/error handlers; scanner likely cannot track event listener patterns"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
tests/fixtures/real_world/javascript/state/event_listener_leak.js
vendored
Normal file
39
tests/fixtures/real_world/javascript/state/event_listener_leak.js
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
var net = require('net');
|
||||
|
||||
function startServer() {
|
||||
var connections = [];
|
||||
var server = net.createServer(function(socket) {
|
||||
connections.push(socket);
|
||||
socket.on('data', function(data) {
|
||||
handleData(socket, data);
|
||||
});
|
||||
// Missing: socket.on('close', ...) cleanup
|
||||
// Missing: socket.on('error', ...) cleanup
|
||||
});
|
||||
server.listen(3000);
|
||||
}
|
||||
|
||||
function startServerSafe() {
|
||||
var connections = [];
|
||||
var server = net.createServer(function(socket) {
|
||||
connections.push(socket);
|
||||
socket.on('data', function(data) {
|
||||
handleData(socket, data);
|
||||
});
|
||||
socket.on('close', function() {
|
||||
var idx = connections.indexOf(socket);
|
||||
if (idx !== -1) {
|
||||
connections.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
socket.on('error', function(err) {
|
||||
console.error('Socket error:', err);
|
||||
socket.destroy();
|
||||
});
|
||||
});
|
||||
server.listen(3000);
|
||||
}
|
||||
|
||||
function handleData(socket, data) {
|
||||
socket.write('echo: ' + data.toString());
|
||||
}
|
||||
25
tests/fixtures/real_world/javascript/state/fd_leak.expect.json
vendored
Normal file
25
tests/fixtures/real_world/javascript/state/fd_leak.expect.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"description": "File descriptor leak in readAndProcess (missing closeSync). readAndClose properly closes the fd.",
|
||||
"tags": [
|
||||
"state",
|
||||
"resource-leak",
|
||||
"fd",
|
||||
"fs"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "state-resource-leak",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
2,
|
||||
10
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "fd from fs.openSync is never closed in readAndProcess; scanner may not track JS fd lifecycle"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
tests/fixtures/real_world/javascript/state/fd_leak.js
vendored
Normal file
17
tests/fixtures/real_world/javascript/state/fd_leak.js
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
var fs = require('fs');
|
||||
|
||||
function readAndProcess(path) {
|
||||
var fd = fs.openSync(path, 'r');
|
||||
var buf = Buffer.alloc(1024);
|
||||
fs.readSync(fd, buf);
|
||||
// Missing: fs.closeSync(fd)
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
function readAndClose(path) {
|
||||
var fd = fs.openSync(path, 'r');
|
||||
var buf = Buffer.alloc(1024);
|
||||
fs.readSync(fd, buf);
|
||||
fs.closeSync(fd);
|
||||
return buf.toString();
|
||||
}
|
||||
37
tests/fixtures/real_world/javascript/state/handle_reuse.expect.json
vendored
Normal file
37
tests/fixtures/real_world/javascript/state/handle_reuse.expect.json
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"description": "Double-close and use-after-close patterns with file descriptors. Both are temporal safety violations.",
|
||||
"tags": [
|
||||
"state",
|
||||
"double-close",
|
||||
"use-after-close",
|
||||
"fd",
|
||||
"fs"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "state-double-close",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
2,
|
||||
8
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "fd closed twice in doubleClose; scanner may not track JS fd state transitions"
|
||||
},
|
||||
{
|
||||
"rule_id": "state-use-after-close",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
8,
|
||||
15
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "fd used in readSync after closeSync in useAfterClose; scanner may not track JS fd state transitions"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
tests/fixtures/real_world/javascript/state/handle_reuse.js
vendored
Normal file
14
tests/fixtures/real_world/javascript/state/handle_reuse.js
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
var fs = require('fs');
|
||||
|
||||
function doubleClose(path) {
|
||||
var fd = fs.openSync(path, 'r');
|
||||
fs.closeSync(fd);
|
||||
fs.closeSync(fd); // double close!
|
||||
}
|
||||
|
||||
function useAfterClose(path) {
|
||||
var fd = fs.openSync(path, 'r');
|
||||
fs.closeSync(fd);
|
||||
var buf = Buffer.alloc(1024);
|
||||
fs.readSync(fd, buf); // use after close!
|
||||
}
|
||||
35
tests/fixtures/real_world/javascript/taint/cmdi_express.expect.json
vendored
Normal file
35
tests/fixtures/real_world/javascript/taint/cmdi_express.expect.json
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"description": "Express req.query flows into child_process.exec (command injection). Safe version uses regex replace but scanner lacks custom sanitizer recognition.",
|
||||
"tags": [
|
||||
"taint",
|
||||
"cmdi",
|
||||
"express"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
4,
|
||||
10
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "req.query.host flows directly into child_process.exec via string concatenation"
|
||||
},
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
12,
|
||||
19
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "Safe version still fires because .replace is not a recognized sanitizer for SHELL_ESCAPE cap"
|
||||
}
|
||||
]
|
||||
}
|
||||
18
tests/fixtures/real_world/javascript/taint/cmdi_express.js
vendored
Normal file
18
tests/fixtures/real_world/javascript/taint/cmdi_express.js
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
var child_process = require('child_process');
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
|
||||
app.get('/ping', function(req, res) {
|
||||
var host = req.query.host;
|
||||
child_process.exec('ping -c 1 ' + host, function(err, stdout) {
|
||||
res.send(stdout);
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/safe-ping', function(req, res) {
|
||||
var host = req.query.host;
|
||||
var sanitized = host.replace(/[^a-zA-Z0-9.]/g, '');
|
||||
child_process.exec('ping -c 1 ' + sanitized, function(err, stdout) {
|
||||
res.send(stdout);
|
||||
});
|
||||
});
|
||||
36
tests/fixtures/real_world/javascript/taint/eval_user_input.expect.json
vendored
Normal file
36
tests/fixtures/real_world/javascript/taint/eval_user_input.expect.json
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"description": "eval() with user-controlled input from req.query. AST pattern detects eval call; taint detects source-to-sink flow.",
|
||||
"tags": [
|
||||
"taint",
|
||||
"code-exec",
|
||||
"eval",
|
||||
"express"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "js.code_exec.eval",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
4,
|
||||
8
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "AST pattern matches eval() call regardless of arguments"
|
||||
},
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
3,
|
||||
8
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "req.query.expr flows directly into eval() which is a SHELL_ESCAPE sink"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
tests/fixtures/real_world/javascript/taint/eval_user_input.js
vendored
Normal file
17
tests/fixtures/real_world/javascript/taint/eval_user_input.js
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
var express = require('express');
|
||||
var app = express();
|
||||
|
||||
app.get('/calc', function(req, res) {
|
||||
var expr = req.query.expr;
|
||||
var result = eval(expr);
|
||||
res.json({ result: result });
|
||||
});
|
||||
|
||||
app.get('/calc-safe', function(req, res) {
|
||||
var expr = req.query.expr;
|
||||
var num = parseFloat(expr);
|
||||
if (isNaN(num)) {
|
||||
return res.status(400).send('Invalid');
|
||||
}
|
||||
res.json({ result: num });
|
||||
});
|
||||
36
tests/fixtures/real_world/javascript/taint/path_traversal_fs.expect.json
vendored
Normal file
36
tests/fixtures/real_world/javascript/taint/path_traversal_fs.expect.json
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"description": "Path traversal via req.query flowing into fs.readFileSync. Scanner lacks fs.readFileSync as a defined sink.",
|
||||
"tags": [
|
||||
"taint",
|
||||
"path-traversal",
|
||||
"express",
|
||||
"fs"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
5,
|
||||
10
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "req.query.path flows into fs.readFileSync but fs.readFileSync is not a recognized taint sink"
|
||||
},
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
11,
|
||||
20
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "Safe version uses path.resolve and startsWith guard; would require adding fs sinks to detect"
|
||||
}
|
||||
]
|
||||
}
|
||||
20
tests/fixtures/real_world/javascript/taint/path_traversal_fs.js
vendored
Normal file
20
tests/fixtures/real_world/javascript/taint/path_traversal_fs.js
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
var express = require('express');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var app = express();
|
||||
|
||||
app.get('/read', function(req, res) {
|
||||
var filePath = req.query.path;
|
||||
var content = fs.readFileSync(filePath, 'utf8');
|
||||
res.send(content);
|
||||
});
|
||||
|
||||
app.get('/read-safe', function(req, res) {
|
||||
var filePath = req.query.path;
|
||||
var resolved = path.resolve('/safe/dir', filePath);
|
||||
if (!resolved.startsWith('/safe/dir')) {
|
||||
return res.status(403).send('Forbidden');
|
||||
}
|
||||
var content = fs.readFileSync(resolved, 'utf8');
|
||||
res.send(content);
|
||||
});
|
||||
35
tests/fixtures/real_world/javascript/taint/proto_pollution.expect.json
vendored
Normal file
35
tests/fixtures/real_world/javascript/taint/proto_pollution.expect.json
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"description": "Prototype pollution via recursive merge of user-controlled req.body. The __proto__ assignment is indirect (dynamic key).",
|
||||
"tags": [
|
||||
"taint",
|
||||
"prototype-pollution",
|
||||
"express"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "js.prototype.proto_assignment",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
2,
|
||||
8
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "Dynamic property assignment target[key] could pollute __proto__ but AST pattern only matches literal __proto__ property access"
|
||||
},
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
13,
|
||||
19
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "req.body flows through merge into config but no recognized sink is reached"
|
||||
}
|
||||
]
|
||||
}
|
||||
19
tests/fixtures/real_world/javascript/taint/proto_pollution.js
vendored
Normal file
19
tests/fixtures/real_world/javascript/taint/proto_pollution.js
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
function merge(target, source) {
|
||||
for (var key in source) {
|
||||
if (typeof source[key] === 'object') {
|
||||
target[key] = merge(target[key] || {}, source[key]);
|
||||
} else {
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
|
||||
app.post('/config', function(req, res) {
|
||||
var defaults = { theme: 'light', lang: 'en' };
|
||||
var config = merge(defaults, req.body);
|
||||
res.json(config);
|
||||
});
|
||||
24
tests/fixtures/real_world/javascript/taint/sqli_concat.expect.json
vendored
Normal file
24
tests/fixtures/real_world/javascript/taint/sqli_concat.expect.json
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"description": "SQL injection via string concatenation with userId parameter. connection.query is not a recognized taint sink.",
|
||||
"tags": [
|
||||
"taint",
|
||||
"sqli",
|
||||
"mysql"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
2,
|
||||
7
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "userId flows into SQL string via concat, but connection.query is not a defined sink and userId as a function param is not auto-tainted"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
tests/fixtures/real_world/javascript/taint/sqli_concat.js
vendored
Normal file
14
tests/fixtures/real_world/javascript/taint/sqli_concat.js
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
var mysql = require('mysql');
|
||||
|
||||
function getUser(connection, userId) {
|
||||
var query = 'SELECT * FROM users WHERE id = ' + userId;
|
||||
connection.query(query, function(err, results) {
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
||||
function getUserSafe(connection, userId) {
|
||||
connection.query('SELECT * FROM users WHERE id = ?', [userId], function(err, results) {
|
||||
return results;
|
||||
});
|
||||
}
|
||||
36
tests/fixtures/real_world/javascript/taint/xss_res_send.expect.json
vendored
Normal file
36
tests/fixtures/real_world/javascript/taint/xss_res_send.expect.json
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"description": "XSS via req.query flowing into innerHTML. DOMPurify.sanitize is a recognized HTML_ESCAPE sanitizer.",
|
||||
"tags": [
|
||||
"taint",
|
||||
"xss",
|
||||
"express",
|
||||
"innerHTML"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
3,
|
||||
8
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "req.query.name flows into innerHTML via string concatenation"
|
||||
},
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
8,
|
||||
14
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "DOMPurify.sanitize strips HTML_ESCAPE cap so this should NOT fire; must_match=false means we expect absence"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
tests/fixtures/real_world/javascript/taint/xss_res_send.js
vendored
Normal file
13
tests/fixtures/real_world/javascript/taint/xss_res_send.js
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
var express = require('express');
|
||||
var app = express();
|
||||
|
||||
app.get('/greet', function(req, res) {
|
||||
var name = req.query.name;
|
||||
document.getElementById('header').innerHTML = '<h1>Hello ' + name + '</h1>';
|
||||
});
|
||||
|
||||
app.get('/greet-safe', function(req, res) {
|
||||
var name = req.query.name;
|
||||
var clean = DOMPurify.sanitize(name);
|
||||
document.getElementById('header').innerHTML = '<h1>Hello ' + clean + '</h1>';
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue