mirror of
https://github.com/Cacti/cacti.git
synced 2026-11-02 18:17:00 +09:00
* security: require CSRF-protected POST for link and plugin reordering (#7454) Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(csrf): share POST and token guards for reorder actions Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
60 lines
3.0 KiB
PHP
60 lines
3.0 KiB
PHP
<?php
|
|
// Copyright (C) 2026 The Cacti Group. GPL version 2 or later.
|
|
|
|
test('link and plugin reorders reject GET and invalid tokens before writing (#7454)', function () {
|
|
$root = dirname(__DIR__, 4);
|
|
$parser = (new PhpParser\ParserFactory())->createForNewestSupportedVersion();
|
|
$finder = new PhpParser\NodeFinder();
|
|
$printer = new PhpParser\PrettyPrinter\Standard();
|
|
$helper = $finder->findFirst($parser->parse(file_get_contents($root . '/include/csrf.php')),
|
|
static fn ($node) => $node instanceof PhpParser\Node\Stmt\Function_ && $node->name->toString() === 'csrf_require_post');
|
|
expect($helper)->not->toBeNull();
|
|
$guard = $printer->prettyPrint([$helper]);
|
|
|
|
foreach (['links.php', 'plugins.php'] as $file) {
|
|
$nodes = $parser->parse(file_get_contents($root . '/' . $file));
|
|
$case = $finder->findFirst($nodes, static fn ($node) => $node instanceof PhpParser\Node\Stmt\Case_
|
|
&& $node->cond instanceof PhpParser\Node\Scalar\String_ && $node->cond->value === 'ajax_dnd');
|
|
expect($case)->not->toBeNull();
|
|
$body = $printer->prettyPrint($case->stmts);
|
|
|
|
foreach ([['GET', null, 405], ['GET', 'valid', 405], ['HEAD', 'valid', 405], ['PUT', 'valid', 405], [null, 'valid', 405], ['POST', null, 403], ['POST', 'invalid', 403], ['POST', 'valid', 302]] as $scenario) {
|
|
[$method, $token, $status] = $scenario;
|
|
$seed = var_export(['method' => $method, 'token' => $token], true);
|
|
$script = '<?php $seed = ' . $seed . ';' . <<<'PHP'
|
|
|
|
$_SERVER['REQUEST_METHOD'] = $seed['method'];
|
|
http_response_code(200);
|
|
$writes = 0;
|
|
register_shutdown_function(function () { echo json_encode(array('status' => http_response_code(), 'writes' => $GLOBALS['writes'])); });
|
|
function csrf_check($fatal) { return $GLOBALS['seed']['token'] === 'valid'; }
|
|
function gnrv($name) { return array(2, 1); }
|
|
function links_reorder($order) { $GLOBALS['writes']++; }
|
|
function api_plugin_reorder($order) { $GLOBALS['writes']++; }
|
|
PHP;
|
|
$script .= "\n" . $guard . "\nswitch ('ajax_dnd') { case 'ajax_dnd':\n" . $body . "\n}";
|
|
$process = proc_open([PHP_BINARY], [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
|
|
fwrite($pipes[0], $script);
|
|
fclose($pipes[0]);
|
|
$result = json_decode(stream_get_contents($pipes[1]), true);
|
|
$stderr = stream_get_contents($pipes[2]);
|
|
fclose($pipes[1]);
|
|
fclose($pipes[2]);
|
|
expect(proc_close($process))->toBe(0);
|
|
expect($stderr)->toBe('');
|
|
expect($result)->toBe(['status' => $status, 'writes' => $status === 302 ? 1 : 0]);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('reorder clients submit their action and serialized order through the CSRF POST helper', function () {
|
|
$root = dirname(__DIR__, 4);
|
|
|
|
foreach (['links.php', 'plugins.php'] as $file) {
|
|
$source = file_get_contents($root . '/' . $file);
|
|
expect($source)->toContain("cactiPreparePostRequest('" . $file . "', 'action=ajax_dnd&' + $.tableDnD.serialize())");
|
|
expect($source)->toContain('postUrl({url: request.url}, request.data)');
|
|
expect($source)->not->toContain("loadUrl({url:'" . $file . '?action=ajax_dnd');
|
|
}
|
|
});
|