|
13 | 13 | */ |
14 | 14 |
|
15 | 15 | /* |
16 | | - * Tests for the backtick-to-shell_exec migration in scripts/sql.php |
17 | | - * and scripts/ss_sql.php. |
| 16 | + * Tests for scripts/sql.php and scripts/ss_sql.php. |
18 | 17 | * |
19 | | - * PHP 8.4 deprecates the backtick operator. These scripts previously |
20 | | - * used backticks to invoke mysqladmin, with unescaped variable |
21 | | - * interpolation. The fix replaces backticks with shell_exec() and |
22 | | - * wraps all user-supplied values in escapeshellarg(). |
| 18 | + * Covers two hardening steps: |
| 19 | + * 1. Backtick-to-shell_exec migration (PHP 8.4 deprecates the backtick |
| 20 | + * operator; the fix replaces backticks with shell_exec()). |
| 21 | + * 2. cacti_escapeshellarg() wrapper (Cacti's internal contract; bare |
| 22 | + * escapeshellarg() bypasses any future Cacti-level escaping hooks). |
23 | 23 | */ |
24 | 24 |
|
25 | 25 | $sqlPhpPath = __DIR__ . '/../../scripts/sql.php'; |
|
39 | 39 | expect($contents)->toContain('shell_exec('); |
40 | 40 | }); |
41 | 41 |
|
42 | | -test('sql.php escapes database_hostname with escapeshellarg', function () use ($sqlPhpPath) { |
| 42 | +test('sql.php escapes database_hostname with cacti_escapeshellarg', function () use ($sqlPhpPath) { |
43 | 43 | $contents = file_get_contents($sqlPhpPath); |
44 | 44 |
|
45 | | - expect($contents)->toContain('escapeshellarg($database_hostname)'); |
| 45 | + expect($contents)->toContain('cacti_escapeshellarg($database_hostname)'); |
46 | 46 | }); |
47 | 47 |
|
48 | | -test('sql.php escapes database_username with escapeshellarg', function () use ($sqlPhpPath) { |
| 48 | +test('sql.php escapes database_username with cacti_escapeshellarg', function () use ($sqlPhpPath) { |
49 | 49 | $contents = file_get_contents($sqlPhpPath); |
50 | 50 |
|
51 | | - expect($contents)->toContain('escapeshellarg($database_username)'); |
| 51 | + expect($contents)->toContain('cacti_escapeshellarg($database_username)'); |
52 | 52 | }); |
53 | 53 |
|
54 | | -test('sql.php escapes database_password with escapeshellarg', function () use ($sqlPhpPath) { |
| 54 | +test('sql.php escapes database_password with cacti_escapeshellarg', function () use ($sqlPhpPath) { |
55 | 55 | $contents = file_get_contents($sqlPhpPath); |
56 | 56 |
|
57 | | - expect($contents)->toContain('escapeshellarg($database_password)'); |
| 57 | + expect($contents)->toContain('cacti_escapeshellarg($database_password)'); |
| 58 | +}); |
| 59 | + |
| 60 | +test('sql.php uses no bare escapeshellarg calls', function () use ($sqlPhpPath) { |
| 61 | + $contents = file_get_contents($sqlPhpPath); |
| 62 | + |
| 63 | + // Negative lookbehind: match escapeshellarg( NOT preceded by cacti_ |
| 64 | + expect(preg_match('/(?<!cacti_)escapeshellarg\(/', $contents))->toBe(0); |
58 | 65 | }); |
59 | 66 |
|
60 | 67 | test('sql.php handles null return from shell_exec', function () use ($sqlPhpPath) { |
|
63 | 70 | expect($contents)->toContain("?? ''"); |
64 | 71 | }); |
65 | 72 |
|
| 73 | +test('sql.php returns U on empty/null shell_exec output', function () use ($sqlPhpPath) { |
| 74 | + $contents = file_get_contents($sqlPhpPath); |
| 75 | + |
| 76 | + /* Cacti data source scripts must return 'U' on error, never empty string. */ |
| 77 | + expect($contents)->toContain(": 'U'"); |
| 78 | +}); |
| 79 | + |
66 | 80 | // --- scripts/ss_sql.php: no backtick operators remain --- |
67 | 81 |
|
68 | 82 | test('ss_sql.php contains no backtick operators', function () use ($ssSqlPhpPath) { |
|
77 | 91 | expect($contents)->toContain('shell_exec('); |
78 | 92 | }); |
79 | 93 |
|
80 | | -test('ss_sql.php escapes database_hostname with escapeshellarg', function () use ($ssSqlPhpPath) { |
| 94 | +test('ss_sql.php escapes database_hostname with cacti_escapeshellarg', function () use ($ssSqlPhpPath) { |
| 95 | + $contents = file_get_contents($ssSqlPhpPath); |
| 96 | + |
| 97 | + expect($contents)->toContain('cacti_escapeshellarg($database_hostname)'); |
| 98 | +}); |
| 99 | + |
| 100 | +test('ss_sql.php escapes database_username with cacti_escapeshellarg', function () use ($ssSqlPhpPath) { |
81 | 101 | $contents = file_get_contents($ssSqlPhpPath); |
82 | 102 |
|
83 | | - expect($contents)->toContain('escapeshellarg($database_hostname)'); |
| 103 | + expect($contents)->toContain('cacti_escapeshellarg($database_username)'); |
84 | 104 | }); |
85 | 105 |
|
86 | | -test('ss_sql.php escapes database_username with escapeshellarg', function () use ($ssSqlPhpPath) { |
| 106 | +test('ss_sql.php escapes database_password with cacti_escapeshellarg', function () use ($ssSqlPhpPath) { |
87 | 107 | $contents = file_get_contents($ssSqlPhpPath); |
88 | 108 |
|
89 | | - expect($contents)->toContain('escapeshellarg($database_username)'); |
| 109 | + expect($contents)->toContain('cacti_escapeshellarg($database_password)'); |
90 | 110 | }); |
91 | 111 |
|
92 | | -test('ss_sql.php escapes database_password with escapeshellarg', function () use ($ssSqlPhpPath) { |
| 112 | +test('ss_sql.php uses no bare escapeshellarg calls', function () use ($ssSqlPhpPath) { |
93 | 113 | $contents = file_get_contents($ssSqlPhpPath); |
94 | 114 |
|
95 | | - expect($contents)->toContain('escapeshellarg($database_password)'); |
| 115 | + // Negative lookbehind: match escapeshellarg( NOT preceded by cacti_ |
| 116 | + expect(preg_match('/(?<!cacti_)escapeshellarg\(/', $contents))->toBe(0); |
96 | 117 | }); |
97 | 118 |
|
98 | 119 | test('ss_sql.php handles null return from shell_exec', function () use ($ssSqlPhpPath) { |
|
101 | 122 | expect($contents)->toContain("?? ''"); |
102 | 123 | }); |
103 | 124 |
|
| 125 | +test('ss_sql.php returns U on empty/null shell_exec output', function () use ($ssSqlPhpPath) { |
| 126 | + $contents = file_get_contents($ssSqlPhpPath); |
| 127 | + |
| 128 | + /* Cacti data source scripts must return 'U' on error, never empty string. */ |
| 129 | + expect($contents)->toContain(": 'U'"); |
| 130 | +}); |
| 131 | + |
| 132 | +// --- runtime: cacti_escapeshellarg is callable and ss_sql() falls back to 'U' --- |
| 133 | + |
| 134 | +test('ss_sql() returns U when shell_exec produces no output', function () use ($ssSqlPhpPath) { |
| 135 | + /* Bootstrap cacti_escapeshellarg if global.php has not yet been loaded. */ |
| 136 | + if (!function_exists('cacti_escapeshellarg')) { |
| 137 | + /* Minimal stub: delegate to the native call so arg-quoting still works. */ |
| 138 | + function cacti_escapeshellarg(string $arg, bool $quote = true): string { |
| 139 | + return escapeshellarg($arg); |
| 140 | + } |
| 141 | + } |
| 142 | + |
| 143 | + /* Provide dummy globals so the function can build its command string. */ |
| 144 | + $GLOBALS['database_hostname'] = '127.0.0.1'; |
| 145 | + $GLOBALS['database_username'] = 'cacti_test_no_such_user'; |
| 146 | + $GLOBALS['database_password'] = ''; |
| 147 | + |
| 148 | + /* Include the script in "called by script server" mode so only the |
| 149 | + * function definition is loaded, not the top-level print statement. */ |
| 150 | + $called_by_script_server = true; |
| 151 | + if (!function_exists('ss_sql')) { |
| 152 | + require $ssSqlPhpPath; |
| 153 | + } |
| 154 | + |
| 155 | + /* mysqladmin will fail (bad credentials / no server), shell_exec returns |
| 156 | + * null or empty. ss_sql() must map that to 'U'. */ |
| 157 | + expect(ss_sql())->toBe('U'); |
| 158 | +}); |
| 159 | + |
104 | 160 | // --- no raw variable interpolation in shell commands --- |
105 | 161 |
|
106 | 162 | test('sql.php does not interpolate variables directly in shell strings', function () use ($sqlPhpPath) { |
|
0 commit comments