time(), 'end_time' => time(), 'data_source' => "traffic_in' AND 1=(SELECT password FROM user_auth LIMIT 1)-- ", 'cf' => 'avg', 'measure' => 'average', 'order' => 'asc', ]); // db_qstr() must have backslash-escaped the embedded quote so the // payload stays inside the string literal instead of closing it. expect($join)->toContain("\\'") ->and($join)->not->toContain("rrd_name = 'traffic_in' AND"); }); test('malicious measure/order survive the full order-fragment build as a safe clause', function () { [, $order] = build_dsstats_order_fragment([ 'start_time' => time(), 'end_time' => time(), 'data_source' => 'traffic_in', 'cf' => 'avg', 'measure' => '1) UNION SELECT password FROM user_auth-- ', 'order' => 'asc; DROP TABLE user_auth-- ', ]); expect($order)->toBe('ORDER BY rs.average ASC') ->and($order)->not->toContain('UNION') ->and($order)->not->toContain('DROP') ->and($order)->not->toContain(';'); }); test('a quote-breakout data_source combined with a statement-terminator measure stays inert in both halves of the fragment', function () { [$join, $order] = build_dsstats_order_fragment([ 'start_time' => time(), 'end_time' => time(), 'data_source' => "x'; DROP TABLE user_auth; -- ", 'cf' => 'max', 'measure' => 'p95n; DROP TABLE user_auth-- ', 'order' => 'desc', ]); // db_qstr() escapes the embedded quote, so "DROP TABLE" stays inert // string data inside 'x\'; DROP TABLE user_auth; -- ' rather than // breaking out into executable SQL. What must NOT happen is an // *unescaped* quote reaching the JOIN, or the measure's payload // reaching the ORDER BY at all. expect($join)->toContain("x\\'; DROP TABLE user_auth; -- ") ->and($join)->not->toContain("rrd_name = 'x';"); expect($order)->toBe('ORDER BY rs.average DESC') ->and($order)->not->toContain('DROP') ->and($order)->not->toContain(';'); }); test('lib/auth.php builds the dsstats ORDER BY via get_dsstats_order_string(), not raw concatenation', function () { $contents = file_get_contents(dirname(__DIR__, 2) . '/lib/auth.php'); expect($contents)->toContain('get_dsstats_order_string($sql_order)') ->and($contents)->not->toContain("'ORDER BY rs.' . \$sql_order['measure']"); });