toBe('hostname'); }); test('sanitize_sql_column: qualified column passes unchanged', function () { expect(stub_sanitize_sql_column('report.name'))->toBe('report.name'); }); test('sanitize_sql_column: function call with parens passes', function () { expect(stub_sanitize_sql_column('INET_ATON(ip)'))->toBe('INET_ATON(ip)'); }); test('sanitize_sql_column: NATURAL_SORT_KEY wrapper passes', function () { expect(stub_sanitize_sql_column('NATURAL_SORT_KEY(hostname)'))->toBe('NATURAL_SORT_KEY(hostname)'); }); test('sanitize_sql_column: subquery payload bypassing GHSA-q9xg-p762-9jm3 is rejected (GHSA-8j73-cvf6-jrvx)', function () { $payload = '(SELECT(0)FROM(SELECT(SLEEP(5)))a)'; $output = stub_sanitize_sql_column($payload); expect($output)->not->toBe($payload); expect(strtoupper($output))->not->toContain('SELECT'); expect(strtoupper($output))->not->toContain('SLEEP'); }); test('sanitize_sql_column: REGEXP exfiltration subquery payload is rejected (GHSA-8j73-cvf6-jrvx)', function () { $payload = '(SELECT(0)FROM(SELECT(SLEEP(5))FROM(user_auth)WHERE(BINARY(username)REGEXP(0x5e61646d696e24)))a)'; $output = stub_sanitize_sql_column($payload); expect(strtoupper($output))->not->toContain('SELECT'); expect(strtoupper($output))->not->toContain('FROM'); }); test('sanitize_sql_column: a function name that is not on the wrapper allowlist is rejected', function () { expect(stub_sanitize_sql_column('SLEEP(5)'))->not->toBe('SLEEP(5)'); }); test('sanitize_sql_column: column with underscore and digits passes', function () { expect(stub_sanitize_sql_column('last_started_2'))->toBe('last_started_2'); }); // --- sanitize_sql_column: injection payloads stripped --- test('sanitize_sql_column: UNION SELECT payload: spaces and dashes stripped', function () { $input = 'name UNION SELECT password FROM user_auth-- '; $output = stub_sanitize_sql_column($input); // Spaces removed: SQL keywords cannot be parsed as separate tokens without spaces expect($output)->not->toContain(' '); expect($output)->not->toContain('-'); expect($output)->not->toContain(';'); }); test('sanitize_sql_column: semicolon stripped', function () { expect(stub_sanitize_sql_column('name; DROP TABLE reports'))->not->toContain(';'); }); test('sanitize_sql_column: single quote stripped', function () { expect(stub_sanitize_sql_column("name' OR '1'='1"))->not->toContain("'"); }); test('sanitize_sql_column: backtick stripped', function () { expect(stub_sanitize_sql_column('`report`.`name`'))->not->toContain('`'); }); test('sanitize_sql_column: comment marker stripped', function () { $output = stub_sanitize_sql_column('name--comment'); expect($output)->not->toContain('-'); }); test('sanitize_sql_column: empty string returns empty string', function () { expect(stub_sanitize_sql_column(''))->toBe(''); }); test('sanitize_sql_column: null-byte stripped', function () { expect(stub_sanitize_sql_column("name\x00injection"))->not->toContain("\x00"); }); // --- Direction enforcement: valid values --- test('safe_direction: ASC preserved', function () { expect(stub_safe_direction('ASC'))->toBe('ASC'); }); test('safe_direction: DESC preserved', function () { expect(stub_safe_direction('DESC'))->toBe('DESC'); }); test('safe_direction: lowercase asc normalised to ASC', function () { expect(stub_safe_direction('asc'))->toBe('ASC'); }); test('safe_direction: lowercase desc normalised to DESC', function () { expect(stub_safe_direction('desc'))->toBe('DESC'); }); test('safe_direction: mixed case Desc normalised to DESC', function () { expect(stub_safe_direction('Desc'))->toBe('DESC'); }); // --- Direction enforcement: injection payloads default to ASC --- test('safe_direction: SQL payload defaults to ASC', function () { expect(stub_safe_direction('ASC; DROP TABLE reports-- '))->toBe('ASC'); }); test('safe_direction: UNION payload defaults to ASC', function () { expect(stub_safe_direction('ASC UNION SELECT 1'))->toBe('ASC'); }); test('safe_direction: empty string defaults to ASC', function () { expect(stub_safe_direction(''))->toBe('ASC'); }); test('safe_direction: arbitrary string defaults to ASC', function () { expect(stub_safe_direction('RANDOM'))->toBe('ASC'); }); test('safe_direction: null-byte payload defaults to ASC', function () { expect(stub_safe_direction("DESC\x00extra"))->toBe('ASC'); }); // --- Combined: verify sanitized column + direction cannot form injected SQL --- test('combined: injection payload in column and direction produces safe ORDER BY', function () { $raw_col = 'name UNION SELECT password FROM user_auth-- '; $raw_dir = 'ASC; DROP TABLE reports'; $col = stub_sanitize_sql_column($raw_col); $dir = stub_safe_direction($raw_dir); $clause = 'ORDER BY ' . $col . ' ' . $dir; // No spaces in $col: SQL keywords cannot act as separate tokens expect($col)->not->toContain(' '); // No injection-enabling characters survive in $col expect($col)->not->toContain(';'); expect($col)->not->toContain('-'); expect($col)->not->toContain("'"); // Direction is strictly ASC or DESC regardless of payload expect($dir)->toBe('ASC'); // The assembled clause has no injected semicolons or comment markers expect($clause)->not->toContain(';'); expect($clause)->not->toContain('--'); }); // --- Stale-session READ path (mirror of update_order_string() read loops) --- // // This mirrors the read-loop direction normalization in the same stub style as // the rest of this file, without a database, session, or full Cacti bootstrap. // It does not execute update_order_string(); the enforcement lives in that // function's two read loops: // // $direction = strtoupper($direction) === 'DESC' ? 'DESC' : 'ASC'; // // which re-normalize a direction left in $_SESSION['sort_data'][$page] by a // session poisoned before the write-time fix shipped, symmetric with the column // re-validation already applied there. Direction passthrough and injection // defaults are covered above; here the stored value flows into an ORDER BY clause. test('read path: poisoned stored direction yields a safe ORDER BY clause', function () { $column = stub_sanitize_sql_column('report.name'); $dir = stub_safe_direction('ASC; DROP TABLE reports-- '); $clause = 'ORDER BY ' . $column . ' ' . $dir; expect($dir)->toBe('ASC'); expect($clause)->not->toContain(';'); expect($clause)->not->toContain('--'); });