Files
cacti/tests/Unit/PluginsFilterLikeSqliTest.php
Thomas VincentandTheWitness 4238aadcb1 security: escape LIKE wildcards in poller_automation and plugins.php filters (#7670)
* security: add db_like_escape and bind the PTR hostname in poller_automation

poller_automation.php concatenated the gethostbyaddr() result into a LIKE clause, so an attacker controlling the reverse-DNS zone injected SQL through a crafted PTR record. The new db_like_escape() helper neutralises the LIKE metacharacters, and the reverse-DNS hostname is now passed as a bound parameter (LIKE ?). The escape semantics are covered by unit tests and verified against a live MariaDB.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* security: bind and LIKE-escape the plugins.php filter (GHSA-jjfq-mj5r-2r95)

The plugins.php filter reached its LIKE clauses through db_qstr(), which escapes quotes but not the LIKE metacharacters, so a filter of '%' matched every row and bypassed the state filter. The filter LIKE clauses are now bound placeholders and the value is LIKE-escaped once with db_like_escape() and reused for each placeholder; the count and list queries are prepared.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
2026-08-16 21:15:44 -04:00

34 lines
1.6 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
/*
* plugins.php built its filter into LIKE clauses with db_qstr(), which escapes
* quotes but not the LIKE metacharacters % and _, so a filter of '%' matched
* every row and bypassed the state filter (GHSA-jjfq-mj5r-2r95). The filter is
* now a bound parameter, LIKE-escaped via db_like_escape().
*/
$src = file_get_contents(dirname(__DIR__, 2) . '/plugins.php');
test('the filter no longer reaches LIKE through db_qstr', function () use ($src) {
expect($src)->not->toContain("db_qstr('%' . grv('filter') . '%')");
});
test('the filter is bound and LIKE-escaped once, then reused for every placeholder', function () use ($src) {
expect($src)->toContain("array_fill(0, substr_count(\$sql_where, '?'), '%' . db_like_escape(grv('filter')) . '%')");
});
test('every query that embeds the filter where-clause is prepared', function () use ($src) {
// the count and list queries must pass $where_params
expect(substr_count($src, '$sql_where", $where_params);'))->toBeGreaterThanOrEqual(3);
expect($src)->toContain('db_fetch_assoc_prepared($sql, $where_params);');
// and none of them may run the where-clause through the non-prepared fetch
expect($src)->not->toContain('db_fetch_cell("SELECT COUNT(*)');
});