mirror of
https://github.com/elicpeter/nyx.git
synced 2026-07-12 21:02:11 +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
25
tests/fixtures/real_world/php/cfg/curl_lifecycle.expect.json
vendored
Normal file
25
tests/fixtures/real_world/php/cfg/curl_lifecycle.expect.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"description": "cURL handle resource leak - missing curl_close vs properly closed",
|
||||
"tags": [
|
||||
"cfg",
|
||||
"resource-leak",
|
||||
"curl",
|
||||
"php"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "cfg-resource-leak",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
1,
|
||||
9
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "fetchUrl opens cURL handle but never calls curl_close"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
tests/fixtures/real_world/php/cfg/curl_lifecycle.php
vendored
Normal file
17
tests/fixtures/real_world/php/cfg/curl_lifecycle.php
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
function fetchUrl($url) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$response = curl_exec($ch);
|
||||
// Missing curl_close($ch)
|
||||
return $response;
|
||||
}
|
||||
|
||||
function fetchUrlSafe($url) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $response;
|
||||
}
|
||||
?>
|
||||
37
tests/fixtures/real_world/php/cfg/error_fallthrough.expect.json
vendored
Normal file
37
tests/fixtures/real_world/php/cfg/error_fallthrough.expect.json
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"description": "Error check logs but falls through to dangerous system() call with null result",
|
||||
"tags": [
|
||||
"cfg",
|
||||
"error-fallthrough",
|
||||
"cmdi",
|
||||
"php"
|
||||
],
|
||||
"modes": [
|
||||
"full",
|
||||
"ast"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "cfg-error-fallthrough",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
1,
|
||||
10
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "Error condition logs but does not return, falls through to system() call"
|
||||
},
|
||||
{
|
||||
"rule_id": "php.cmdi.system",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
6,
|
||||
10
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "system() called with potentially null array access"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
tests/fixtures/real_world/php/cfg/error_fallthrough.php
vendored
Normal file
10
tests/fixtures/real_world/php/cfg/error_fallthrough.php
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
function processRequest($data) {
|
||||
$result = json_decode($data, true);
|
||||
if ($result === null) {
|
||||
error_log("Invalid JSON");
|
||||
// falls through!
|
||||
}
|
||||
system($result['command']);
|
||||
}
|
||||
?>
|
||||
38
tests/fixtures/real_world/php/cfg/switch_case.expect.json
vendored
Normal file
38
tests/fixtures/real_world/php/cfg/switch_case.expect.json
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"description": "Switch/case dispatching to dangerous functions eval and system",
|
||||
"tags": [
|
||||
"cfg",
|
||||
"code-exec",
|
||||
"cmdi",
|
||||
"switch",
|
||||
"php"
|
||||
],
|
||||
"modes": [
|
||||
"full",
|
||||
"ast"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "php.code_exec.eval",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
3,
|
||||
7
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "eval() called in switch case with function parameter"
|
||||
},
|
||||
{
|
||||
"rule_id": "php.cmdi.system",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
6,
|
||||
10
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "system() called in switch case with function parameter"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
tests/fixtures/real_world/php/cfg/switch_case.php
vendored
Normal file
17
tests/fixtures/real_world/php/cfg/switch_case.php
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
function handleAction($action, $input) {
|
||||
switch ($action) {
|
||||
case 'eval':
|
||||
eval($input);
|
||||
break;
|
||||
case 'exec':
|
||||
system($input);
|
||||
break;
|
||||
case 'log':
|
||||
error_log($input);
|
||||
break;
|
||||
default:
|
||||
echo "Unknown action";
|
||||
}
|
||||
}
|
||||
?>
|
||||
26
tests/fixtures/real_world/php/cfg/try_catch_finally.expect.json
vendored
Normal file
26
tests/fixtures/real_world/php/cfg/try_catch_finally.expect.json
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"description": "File handle resource management with try/catch/finally vs early return leak",
|
||||
"tags": [
|
||||
"cfg",
|
||||
"resource-leak",
|
||||
"try-finally",
|
||||
"file-io",
|
||||
"php"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "cfg-resource-leak",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
12,
|
||||
24
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "leakyProcess returns null on empty data without closing file handle"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
tests/fixtures/real_world/php/cfg/try_catch_finally.php
vendored
Normal file
23
tests/fixtures/real_world/php/cfg/try_catch_finally.php
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
function processFile($path) {
|
||||
$fh = fopen($path, 'r');
|
||||
try {
|
||||
$data = fread($fh, filesize($path));
|
||||
return $data;
|
||||
} catch (Exception $e) {
|
||||
echo $e->getMessage();
|
||||
} finally {
|
||||
fclose($fh);
|
||||
}
|
||||
}
|
||||
|
||||
function leakyProcess($path) {
|
||||
$fh = fopen($path, 'r');
|
||||
$data = fread($fh, filesize($path));
|
||||
if (empty($data)) {
|
||||
return null; // fh leaked
|
||||
}
|
||||
fclose($fh);
|
||||
return $data;
|
||||
}
|
||||
?>
|
||||
37
tests/fixtures/real_world/php/mixed/upload_cmdi.expect.json
vendored
Normal file
37
tests/fixtures/real_world/php/mixed/upload_cmdi.expect.json
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"description": "File upload with command injection via unsanitized filename in system() call",
|
||||
"tags": [
|
||||
"taint",
|
||||
"cmdi",
|
||||
"file-upload",
|
||||
"php",
|
||||
"mixed"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
1,
|
||||
8
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "$_FILES filename flows through concatenation into system() chmod call"
|
||||
},
|
||||
{
|
||||
"rule_id": "php.cmdi.system",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
4,
|
||||
8
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "system() with string concatenation containing user-controlled filename"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
tests/fixtures/real_world/php/mixed/upload_cmdi.php
vendored
Normal file
8
tests/fixtures/real_world/php/mixed/upload_cmdi.php
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
$filename = $_FILES['file']['name'];
|
||||
$tmp = $_FILES['file']['tmp_name'];
|
||||
$target = '/var/www/uploads/' . $filename;
|
||||
move_uploaded_file($tmp, $target);
|
||||
system('chmod 644 ' . $target); // command injection via filename!
|
||||
echo "File uploaded: " . htmlspecialchars($filename);
|
||||
?>
|
||||
61
tests/fixtures/real_world/php/mixed/web_handler.expect.json
vendored
Normal file
61
tests/fixtures/real_world/php/mixed/web_handler.expect.json
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"description": "Web handler dispatches to system, eval, and fopen based on user input",
|
||||
"tags": [
|
||||
"taint",
|
||||
"state",
|
||||
"cmdi",
|
||||
"code-exec",
|
||||
"resource-leak",
|
||||
"php",
|
||||
"mixed"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "php.cmdi.system",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
4,
|
||||
8
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "system() called with $_POST data"
|
||||
},
|
||||
{
|
||||
"rule_id": "php.code_exec.eval",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
6,
|
||||
10
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "eval() called with $_POST data"
|
||||
},
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
1,
|
||||
8
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "$_POST['data'] flows into system()"
|
||||
},
|
||||
{
|
||||
"rule_id": "state-resource-leak",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
8,
|
||||
14
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "File handle opened in read branch but never closed"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
tests/fixtures/real_world/php/mixed/web_handler.php
vendored
Normal file
14
tests/fixtures/real_world/php/mixed/web_handler.php
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
$action = $_GET['action'];
|
||||
$input = $_POST['data'];
|
||||
|
||||
if ($action === 'exec') {
|
||||
system($input);
|
||||
} elseif ($action === 'eval') {
|
||||
eval($input);
|
||||
} elseif ($action === 'read') {
|
||||
$fh = fopen($input, 'r');
|
||||
echo fread($fh, 4096);
|
||||
// fh leaked
|
||||
}
|
||||
?>
|
||||
26
tests/fixtures/real_world/php/state/branch_leak.expect.json
vendored
Normal file
26
tests/fixtures/real_world/php/state/branch_leak.expect.json
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"description": "File handle leaked in else branch of conditional",
|
||||
"tags": [
|
||||
"state",
|
||||
"resource-leak",
|
||||
"branch",
|
||||
"file-io",
|
||||
"php"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "state-resource-leak-possible",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
1,
|
||||
14
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "File handle closed in if branch but leaked in else branch"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
tests/fixtures/real_world/php/state/branch_leak.php
vendored
Normal file
13
tests/fixtures/real_world/php/state/branch_leak.php
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
function conditionalClose($path, $flag) {
|
||||
$fh = fopen($path, 'r');
|
||||
if ($flag) {
|
||||
$data = fread($fh, 1024);
|
||||
fclose($fh);
|
||||
return $data;
|
||||
} else {
|
||||
return "skipped";
|
||||
// $fh leaked
|
||||
}
|
||||
}
|
||||
?>
|
||||
25
tests/fixtures/real_world/php/state/curl_state.expect.json
vendored
Normal file
25
tests/fixtures/real_world/php/state/curl_state.expect.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"description": "cURL handle used after being closed",
|
||||
"tags": [
|
||||
"state",
|
||||
"use-after-close",
|
||||
"curl",
|
||||
"php"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "state-use-after-close",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
1,
|
||||
9
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "curl_exec called after curl_close on same handle"
|
||||
}
|
||||
]
|
||||
}
|
||||
9
tests/fixtures/real_world/php/state/curl_state.php
vendored
Normal file
9
tests/fixtures/real_world/php/state/curl_state.php
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
function useAfterClose($url) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_close($ch);
|
||||
$response = curl_exec($ch); // use after close!
|
||||
return $response;
|
||||
}
|
||||
?>
|
||||
25
tests/fixtures/real_world/php/state/db_connection.expect.json
vendored
Normal file
25
tests/fixtures/real_world/php/state/db_connection.expect.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"description": "mysqli connection resource leak - missing close vs properly closed",
|
||||
"tags": [
|
||||
"state",
|
||||
"resource-leak",
|
||||
"mysqli",
|
||||
"php"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "state-resource-leak",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
1,
|
||||
8
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "queryUnsafe creates mysqli connection but never calls close()"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
tests/fixtures/real_world/php/state/db_connection.php
vendored
Normal file
15
tests/fixtures/real_world/php/state/db_connection.php
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
function queryUnsafe() {
|
||||
$conn = new mysqli("localhost", "user", "pass", "db");
|
||||
$result = $conn->query("SELECT 1");
|
||||
return $result;
|
||||
// $conn never closed
|
||||
}
|
||||
|
||||
function querySafe() {
|
||||
$conn = new mysqli("localhost", "user", "pass", "db");
|
||||
$result = $conn->query("SELECT 1");
|
||||
$conn->close();
|
||||
return $result;
|
||||
}
|
||||
?>
|
||||
37
tests/fixtures/real_world/php/state/file_handle.expect.json
vendored
Normal file
37
tests/fixtures/real_world/php/state/file_handle.expect.json
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"description": "PHP file handle lifecycle: leak, proper close, double close",
|
||||
"tags": [
|
||||
"state",
|
||||
"resource-leak",
|
||||
"double-close",
|
||||
"file-io",
|
||||
"php"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "state-resource-leak",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
1,
|
||||
7
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "readAndLeak opens file handle but never calls fclose"
|
||||
},
|
||||
{
|
||||
"rule_id": "state-double-close",
|
||||
"severity": null,
|
||||
"must_match": false,
|
||||
"line_range": [
|
||||
13,
|
||||
21
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "doubleClose calls fclose twice on same handle"
|
||||
}
|
||||
]
|
||||
}
|
||||
20
tests/fixtures/real_world/php/state/file_handle.php
vendored
Normal file
20
tests/fixtures/real_world/php/state/file_handle.php
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
function readAndLeak($path) {
|
||||
$fh = fopen($path, 'r');
|
||||
$data = fread($fh, 1024);
|
||||
return $data;
|
||||
}
|
||||
|
||||
function readAndClose($path) {
|
||||
$fh = fopen($path, 'r');
|
||||
$data = fread($fh, 1024);
|
||||
fclose($fh);
|
||||
return $data;
|
||||
}
|
||||
|
||||
function doubleClose($path) {
|
||||
$fh = fopen($path, 'r');
|
||||
fclose($fh);
|
||||
fclose($fh);
|
||||
}
|
||||
?>
|
||||
25
tests/fixtures/real_world/php/taint/cmdi_shell_exec.expect.json
vendored
Normal file
25
tests/fixtures/real_world/php/taint/cmdi_shell_exec.expect.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"description": "Command injection via shell_exec with unsanitized GET parameter",
|
||||
"tags": [
|
||||
"taint",
|
||||
"cmdi",
|
||||
"shell_exec",
|
||||
"php"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
1,
|
||||
6
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "$_GET['cmd'] flows directly into shell_exec without sanitization"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
tests/fixtures/real_world/php/taint/cmdi_shell_exec.php
vendored
Normal file
10
tests/fixtures/real_world/php/taint/cmdi_shell_exec.php
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
$cmd = $_GET['cmd'];
|
||||
$output = shell_exec($cmd);
|
||||
echo $output;
|
||||
|
||||
// Safe version
|
||||
$safe_cmd = escapeshellarg($_GET['safe_cmd']);
|
||||
$safe_output = shell_exec('echo ' . $safe_cmd);
|
||||
echo $safe_output;
|
||||
?>
|
||||
36
tests/fixtures/real_world/php/taint/deser_unserialize.expect.json
vendored
Normal file
36
tests/fixtures/real_world/php/taint/deser_unserialize.expect.json
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"description": "Unsafe deserialization of user-controlled cookie data via unserialize",
|
||||
"tags": [
|
||||
"taint",
|
||||
"deser",
|
||||
"unserialize",
|
||||
"php"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "php.deser.unserialize",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
1,
|
||||
5
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "unserialize() on cookie data enables object injection"
|
||||
},
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
1,
|
||||
5
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "$_COOKIE['session_data'] flows into unserialize()"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
tests/fixtures/real_world/php/taint/deser_unserialize.php
vendored
Normal file
10
tests/fixtures/real_world/php/taint/deser_unserialize.php
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
$data = $_COOKIE['session_data'];
|
||||
$obj = unserialize($data);
|
||||
echo $obj->name;
|
||||
|
||||
// Safe: JSON instead
|
||||
$json_data = $_COOKIE['json_data'];
|
||||
$safe_obj = json_decode($json_data);
|
||||
echo $safe_obj->name;
|
||||
?>
|
||||
58
tests/fixtures/real_world/php/taint/eval_input.expect.json
vendored
Normal file
58
tests/fixtures/real_world/php/taint/eval_input.expect.json
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"description": "eval() called with user-controlled POST and GET input",
|
||||
"tags": [
|
||||
"taint",
|
||||
"code-exec",
|
||||
"eval",
|
||||
"php"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "php.code_exec.eval",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
1,
|
||||
5
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "eval() with $_POST['code'] - AST pattern match"
|
||||
},
|
||||
{
|
||||
"rule_id": "php.code_exec.eval",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
4,
|
||||
8
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "eval() with $_GET['expr'] concatenated - AST pattern match"
|
||||
},
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
1,
|
||||
5
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "$_POST['code'] flows directly into eval()"
|
||||
},
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
3,
|
||||
8
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "$_GET['expr'] concatenated and flows into eval()"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
tests/fixtures/real_world/php/taint/eval_input.php
vendored
Normal file
8
tests/fixtures/real_world/php/taint/eval_input.php
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
$code = $_POST['code'];
|
||||
eval($code);
|
||||
|
||||
$expr = $_GET['expr'];
|
||||
$result = eval('return ' . $expr . ';');
|
||||
echo $result;
|
||||
?>
|
||||
25
tests/fixtures/real_world/php/taint/file_upload.expect.json
vendored
Normal file
25
tests/fixtures/real_world/php/taint/file_upload.expect.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"description": "Unsafe file upload with user-controlled filename used in move_uploaded_file",
|
||||
"tags": [
|
||||
"taint",
|
||||
"file-upload",
|
||||
"path-traversal",
|
||||
"php"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
1,
|
||||
6
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "$_FILES['upload']['name'] flows into file path without sanitization"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
tests/fixtures/real_world/php/taint/file_upload.php
vendored
Normal file
12
tests/fixtures/real_world/php/taint/file_upload.php
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
$filename = $_FILES['upload']['name'];
|
||||
$target = '/uploads/' . $filename;
|
||||
move_uploaded_file($_FILES['upload']['tmp_name'], $target);
|
||||
echo "Uploaded to: " . $target;
|
||||
|
||||
// Safe version
|
||||
$safe_name = basename($_FILES['upload']['name']);
|
||||
$safe_name = preg_replace('/[^a-zA-Z0-9._-]/', '', $safe_name);
|
||||
$safe_target = '/uploads/' . $safe_name;
|
||||
move_uploaded_file($_FILES['upload']['tmp_name'], $safe_target);
|
||||
?>
|
||||
25
tests/fixtures/real_world/php/taint/include_rfi.expect.json
vendored
Normal file
25
tests/fixtures/real_world/php/taint/include_rfi.expect.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"description": "Remote file inclusion via user-controlled include path",
|
||||
"tags": [
|
||||
"taint",
|
||||
"rfi",
|
||||
"include",
|
||||
"php"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
1,
|
||||
5
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "$_GET['page'] flows directly into include() without whitelist check"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
tests/fixtures/real_world/php/taint/include_rfi.php
vendored
Normal file
11
tests/fixtures/real_world/php/taint/include_rfi.php
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
$page = $_GET['page'];
|
||||
include($page . '.php');
|
||||
|
||||
// Safe version
|
||||
$allowed = ['home', 'about', 'contact'];
|
||||
$safe_page = $_GET['page'];
|
||||
if (in_array($safe_page, $allowed)) {
|
||||
include($safe_page . '.php');
|
||||
}
|
||||
?>
|
||||
25
tests/fixtures/real_world/php/taint/sqli_concat.expect.json
vendored
Normal file
25
tests/fixtures/real_world/php/taint/sqli_concat.expect.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"description": "SQL injection via string concatenation with user input in mysqli query",
|
||||
"tags": [
|
||||
"taint",
|
||||
"sqli",
|
||||
"mysqli",
|
||||
"php"
|
||||
],
|
||||
"modes": [
|
||||
"full"
|
||||
],
|
||||
"expected": [
|
||||
{
|
||||
"rule_id": "taint-unsanitised-flow",
|
||||
"severity": null,
|
||||
"must_match": true,
|
||||
"line_range": [
|
||||
1,
|
||||
6
|
||||
],
|
||||
"evidence_contains": [],
|
||||
"notes": "$_GET['id'] concatenated directly into SQL query string"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
tests/fixtures/real_world/php/taint/sqli_concat.php
vendored
Normal file
13
tests/fixtures/real_world/php/taint/sqli_concat.php
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
$conn = new mysqli("localhost", "user", "pass", "db");
|
||||
$id = $_GET['id'];
|
||||
$result = $conn->query("SELECT * FROM users WHERE id = " . $id);
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
echo $row['name'];
|
||||
}
|
||||
|
||||
// Safe version
|
||||
$stmt = $conn->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$stmt->bind_param("s", $_GET['safe_id']);
|
||||
$stmt->execute();
|
||||
?>
|
||||
Loading…
Add table
Add a link
Reference in a new issue