Files
cacti/tests/Unit/CsrfGetDeleteTest.php
Thomas Vincent a2c8df612e fix(csrf): require POST tokens for state-changing actions (#7991)
* fix(csrf): require POST tokens for item actions

* test(csrf): cover every guarded action path

* test(csrf): keep legacy guard contract extensible

* fix(csrf): hide POST-only targets from crawlers

* test(csrf): reject every crawlable state-action link

* fix(csrf): fail closed without logging crawler probes

* fix(csrf): post data-query item actions safely

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

* fix(csrf): normalize stripped token separators

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

* fix(csrf): distinguish method and token failures

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

* fix(csrf): escape POST action URL attributes

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

* fix(csrf): delegate POST action events

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

* fix(csrf): post data template add action

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

* style(html): align POST button class

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-09-12 15:09:25 -04:00

73 lines
3.3 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. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
/*
* Delete actions (tree delete_node, host gt_remove/query_remove) fired on GET
* with no CSRF token, so a cross-origin <img> or link deleted objects using the
* victim's session cookie. They must arrive by POST with a token now: the
* server rejects the GET, and the UI submits them through the token POST path.
*/
$root = dirname(__DIR__, 2);
test('the server rejects these delete actions on GET without a token', function () use ($root) {
$src = file_get_contents($root . '/include/global.php');
// The GET-method block covers the original delete actions even as the
// protected list grows to include other state-changing actions.
foreach (['save', 'update_data', 'changepassword', 'delete_node', 'gt_remove', 'query_remove', 'remove', 'change_leaf'] as $action) {
expect($src)->toContain("'$action'");
}
// the block still requires the csrf token on POST
expect($src)->toContain("!isset(\$_POST['__csrf_magic'])");
});
test('tree.php deletes a node via a token POST, not GET', function () use ($root) {
$src = file_get_contents($root . '/tree.php');
expect($src)->toContain("\$.post('?action=delete_node', { 'id' : data.node.id, 'tree_id' : \$('#id').val(), '__csrf_magic' : csrfMagicToken })");
expect($src)->not->toContain("\$.get('?action=delete_node'");
});
test('host.php removes graph templates and data queries via a token POST', function () use ($root) {
$src = file_get_contents($root . '/host.php');
// gt_remove and query_remove no longer build a GET url through loadUrl
expect($src)->not->toContain("'host.php?action=gt_remove&id='");
expect($src)->not->toContain("'host.php?action=query_remove&id='");
// they go through postUrl with the csrf token
expect($src)->toContain("url: 'host.php?action=gt_remove'");
expect($src)->toContain("url: 'host.php?action=query_remove'");
expect(substr_count($src, '__csrf_magic: csrfMagicToken'))->toBeGreaterThanOrEqual(4);
});
test('the automation actions guarded here are not linked by GET anywhere', function () use ($root) {
/* 'remove' and 'change_leaf' can be required to post because no page emits
either as a link. An action still reached by a GET link would break the
moment it joined that list, which is why item_remove is not in it. */
$linked = [];
foreach (glob($root . '/*.php') as $page) {
$src = file_get_contents($page);
foreach (["action=remove'", 'action=remove"', 'action=remove&', 'action=change_leaf'] as $needle) {
if (str_contains($src, $needle)) {
$linked[] = basename($page) . ' => ' . $needle;
}
}
}
expect($linked)->toBe([]);
});