Files
cacti/tests/Unit/PathContainmentTest.php
Thomas Vincent cb79286b18 Contain plugin archive writes and validate the repository URL (#7656)
* Contain plugin archive writes and validate the repository URL

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

* Use the full Cacti copyright header and reject non-base repository URLs

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

* Move the path containment helpers into lib/functions.php

They live in lib/functions.php on 1.2.x; keeping a separate module on develop
split the same helpers across two files. plugin_validate_repository_url() goes
to lib/plugins.php with its caller.

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

* ci: refresh the sink baseline for the containment guard

The archive guard shifts eleven sink line numbers in lib/plugins.php.

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

* ci: drop the baseline refresh this branch did not need

The sink and hotspot gates compare on a line-agnostic signature, so pure line
drift never tripped them and refreshing only invites merge conflicts.

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-08-16 21:33:43 -04:00

114 lines
5.5 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
require_once dirname(__DIR__, 2) . '/lib/functions.php';
beforeEach(function () : void {
$this->base = sys_get_temp_dir() . '/cacti_containment_' . bin2hex(random_bytes(6));
mkdir($this->base . '/nested', 0755, true);
file_put_contents($this->base . '/nested/present.txt', 'x');
});
afterEach(function () : void {
foreach (['/nested/present.txt', '/nested/link', '/link', '/nested', ''] as $leaf) {
$path = $this->base . $leaf;
if (is_link($path)) {
unlink($path);
} elseif (is_file($path)) {
unlink($path);
} elseif (is_dir($path)) {
rmdir($path);
}
}
});
test('a plain relative path resolves inside the base', function () : void {
expect(validate_relative_path_within('nested/present.txt', $this->base))
->toBe(realpath($this->base) . '/nested/present.txt');
});
test('a path whose parent exists but file does not is still accepted', function () : void {
expect(validate_relative_path_within('nested/new.txt', $this->base))
->toBe(realpath($this->base) . '/nested/new.txt');
});
test('traversal out of the base is rejected', function () : void {
expect(validate_relative_path_within('../escape.php', $this->base))->toBeFalse();
expect(validate_relative_path_within('nested/../../escape.php', $this->base))->toBeFalse();
expect(validate_relative_path_within('./nested/present.txt', $this->base))->toBeFalse();
});
test('absolute paths and drive letters are rejected', function () : void {
expect(validate_relative_path_within('/etc/passwd', $this->base))->toBeFalse();
expect(validate_relative_path_within('C:/windows/system32', $this->base))->toBeFalse();
expect(validate_relative_path_within('\\\\server\\share', $this->base))->toBeFalse();
});
test('empty, non string and null byte paths are rejected', function () : void {
expect(validate_relative_path_within('', $this->base))->toBeFalse();
expect(validate_relative_path_within(null, $this->base))->toBeFalse();
expect(validate_relative_path_within(['a'], $this->base))->toBeFalse();
expect(validate_relative_path_within("nested/pre\0sent.txt", $this->base))->toBeFalse();
expect(validate_relative_path_within('nested//present.txt', $this->base))->toBeFalse();
});
test('a missing base directory is rejected', function () : void {
expect(validate_relative_path_within('a.txt', $this->base . '/does_not_exist'))->toBeFalse();
});
test('a symlinked segment under the base is rejected', function () : void {
symlink(sys_get_temp_dir(), $this->base . '/link');
expect(validate_relative_path_within('link/anything.txt', $this->base))->toBeFalse();
});
test('a path whose parent cannot be resolved is rejected', function () : void {
expect(validate_relative_path_within('missing_dir/child/file.txt', $this->base))->toBeFalse();
});
test('cacti_path_is_within accepts the base itself and children', function () : void {
expect(cacti_path_is_within($this->base . '/nested', $this->base))->toBeTrue();
expect(cacti_path_is_within($this->base, $this->base))->toBeTrue();
});
test('cacti_path_is_within rejects unresolvable paths and outside paths', function () : void {
expect(cacti_path_is_within($this->base . '/nope', $this->base))->toBeFalse();
expect(cacti_path_is_within($this->base, $this->base . '/nope'))->toBeFalse();
expect(cacti_path_is_within(sys_get_temp_dir(), $this->base))->toBeFalse();
});
test('windows comparison rules can be forced on any platform', function () : void {
expect(cacti_path_is_within($this->base . '/nested', $this->base, true))->toBeTrue();
expect(cacti_path_is_within(sys_get_temp_dir(), $this->base, true))->toBeFalse();
});
test('windows path normalization folds case, separators and long prefixes', function () : void {
expect(cacti_normalize_windows_path('C:\\Cacti\\Plugins\\'))->toBe('c:/cacti/plugins');
expect(cacti_normalize_windows_path('\\\\?\\C:\\Cacti'))->toBe('c:/cacti');
expect(cacti_normalize_windows_path('\\\\?\\UNC\\server\\share'))->toBe('//server/share');
expect(cacti_normalize_windows_path('/'))->toBe('/');
expect(cacti_normalize_windows_path(null))->toBe('');
});