markTestSkipped('CACTI_PATH_RRA does not resolve to a directory this process can create fixtures under'); } expect(data_source_path_within_rra(data_source_path_test_base() . '/host_ds.rrd'))->toBeTrue() ->and(data_source_path_within_rra(data_source_path_test_base() . '/1/host_ds.rrd'))->toBeTrue(); }); test('a not-yet-created file under the RRA directory is still contained', function () use ($rra_ready) : void { if (!$rra_ready) { $this->markTestSkipped('CACTI_PATH_RRA does not resolve to a directory this process can create fixtures under'); } expect(data_source_path_within_rra(data_source_path_test_base() . '/1/new_ds.rrd'))->toBeTrue(); }); test('a not-yet-created intermediate directory under the RRA directory is still contained', function () use ($rra_ready) : void { if (!$rra_ready) { $this->markTestSkipped('CACTI_PATH_RRA does not resolve to a directory this process can create fixtures under'); } $missing = data_source_path_test_base() . '/does_not_exist_' . bin2hex(random_bytes(4)) . '/4242.rrd'; expect(is_dir(dirname($missing)))->toBeFalse() ->and(data_source_path_within_rra($missing))->toBeTrue(); }); test('redundant slashes from a trailing-slash path_rra are contained, not an escape', function () use ($rra_ready) : void { if (!$rra_ready) { $this->markTestSkipped('CACTI_PATH_RRA does not resolve to a directory this process can create fixtures under'); } // A path_rra saved with a trailing slash makes the '/' -> rra . '/' // expansion produce a doubled slash (rra//0/x.rrd); the file is still inside // the RRA directory and must not be reported as escaping it. expect(data_source_path_within_rra(data_source_path_test_base() . '//host_ds.rrd'))->toBeTrue() ->and(data_source_path_within_rra(data_source_path_test_base() . '//1/host_ds.rrd'))->toBeTrue() ->and(data_source_path_within_rra(data_source_path_test_base() . '/1//new_ds.rrd'))->toBeTrue(); }); test('a CACTI_PATH_RRA defined with a trailing slash still contains its files', function () use ($rra_ready) : void { if (!$rra_ready) { $this->markTestSkipped('CACTI_PATH_RRA does not resolve to a directory this process can create fixtures under'); } if (!function_exists('shell_exec') || !defined('PHP_BINARY') || PHP_BINARY === '') { $this->markTestSkipped('shell_exec()/PHP_BINARY unavailable, cannot exercise an isolated process'); } // CACTI_PATH_RRA is an immutable constant, already defined in this process // without a trailing slash (and realpath() would strip one anyway), so the // trailing-slash form - the actual trigger, where the base needs rtrim() and // not just the target - can only be exercised in a fresh process that // defines the constant that way. $rra = data_source_path_test_base(); $functions = dirname(__DIR__, 4) . '/lib/functions.php'; $autoload = dirname(__DIR__, 4) . '/vendor/autoload.php'; $code = sprintf( '&1'); } finally { @unlink($script); } expect(trim((string) $output))->toBe('PASS'); }); test('an absolute path elsewhere is rejected', function () : void { expect(data_source_path_within_rra('/var/www/html/cacti/resource/x.php'))->toBeFalse() ->and(data_source_path_within_rra('/tmp/x.rrd'))->toBeFalse(); }); test('traversal out of the RRA directory is rejected', function () : void { expect(data_source_path_within_rra(data_source_path_test_base() . '/../resource/x.rrd'))->toBeFalse() ->and(data_source_path_within_rra(data_source_path_test_base() . '/a/../../etc/x'))->toBeFalse(); }); test('traversal is still rejected when wrapped in redundant slashes', function () : void { expect(data_source_path_within_rra(data_source_path_test_base() . '//..//resource/x.rrd'))->toBeFalse() ->and(data_source_path_within_rra(data_source_path_test_base() . '/a//..//../etc/x'))->toBeFalse(); }); test('a relative path or a lookalike prefix is rejected', function () : void { expect(data_source_path_within_rra('1/host_ds.rrd'))->toBeFalse() ->and(data_source_path_within_rra(data_source_path_test_base() . '_evil/x.rrd'))->toBeFalse(); }); test('empty and null-byte paths are rejected', function () : void { expect(data_source_path_within_rra(''))->toBeFalse() ->and(data_source_path_within_rra("/tmp/x\0.rrd"))->toBeFalse(); }); test('a symlink pivot below the RRA directory is rejected even for a not-yet-created file', function () use ($rra_ready) : void { if (!$rra_ready) { $this->markTestSkipped('CACTI_PATH_RRA does not resolve to a directory this process can create fixtures under'); } $outside = sys_get_temp_dir() . '/cacti_rra_outside_' . bin2hex(random_bytes(6)); mkdir($outside, 0755, true); $link = data_source_path_test_base() . '/linked'; if (!@symlink($outside, $link)) { $this->markTestSkipped('symlink() is not available in this environment'); } try { expect(data_source_path_within_rra($link . '/escaped.rrd'))->toBeFalse(); } finally { unlink($link); rmdir($outside); } }); test('a dangling symlink below the RRA directory is rejected even for a not-yet-created file', function () use ($rra_ready) : void { if (!$rra_ready) { $this->markTestSkipped('CACTI_PATH_RRA does not resolve to a directory this process can create fixtures under'); } // A dangling symlink (target does not exist) is false for file_exists() but // true for is_link(); the containment must reject a path through it so the // link cannot be pointed outside the RRA root and have its target created // before the recursive directory/RRD build materializes the path. $missing_target = sys_get_temp_dir() . '/cacti_rra_dangling_' . bin2hex(random_bytes(6)); $link = data_source_path_test_base() . '/dangling'; if (!@symlink($missing_target, $link)) { $this->markTestSkipped('symlink() is not available in this environment'); } try { expect(is_link($link))->toBeTrue() ->and(file_exists($link))->toBeFalse() ->and(data_source_path_within_rra($link . '/escaped.rrd'))->toBeFalse(); } finally { unlink($link); } });