Files
cacti/tests/js/csrf_post_actions.test.js
T
TheWitness bfa3efb7a9 Add a jQuery rebind() helper and roll it out across the core UI (#8279)
* Add a jQuery rebind() helper and roll it out across the core UI

Add $.fn.rebind(events[, selector], handler...) to include/layout.js as a
shorthand for the common off(events).on(events, ...) re-attach idiom, then
convert the matching chained calls across the core JavaScript and inline
PHP handlers to use it. Only pairs whose off() and on() events (and
delegated selector) match are converted; off() with no arguments is left
as-is since it intentionally clears every handler.

* changelog: add 8279.feature fragment

* tests: update layout.js JS tests for the rebind() helper

* tests: assert the cactiPostAction handler via rebind()

* rebind: preserve the delegated selector for the two-argument event-map overload; add helper tests
2026-10-10 17:01:49 -04:00

81 lines
3.3 KiB
JavaScript

/*
+-------------------------------------------------------------------------+
| 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 |
+-------------------------------------------------------------------------+
*/
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const vm = require('node:vm');
function extractFunction(source, name) {
const start = source.indexOf(`function ${name}(`);
assert.notEqual(start, -1, `${name}() must exist`);
const bodyStart = source.indexOf('{', start);
let depth = 0;
for (let offset = bodyStart; offset < source.length; offset++) {
if (source[offset] === '{') depth++;
if (source[offset] === '}' && --depth === 0) return source.slice(start, offset + 1);
}
throw new Error(`Unable to extract ${name}()`);
}
const source = fs.readFileSync(path.join(__dirname, '..', '..', 'include', 'layout.js'), 'utf8');
const context = {
URL,
URLSearchParams,
csrfMagicToken: 'trusted-token',
window: {location: {href: 'https://cacti.example/cacti/', origin: 'https://cacti.example'}},
$: {
extend: (target, sourceValue) => Object.assign(target, sourceValue),
param: (fields) => new URLSearchParams(fields.map(({name, value}) => [name, value])).toString(),
},
};
vm.runInNewContext(`${extractFunction(source, 'cactiPreparePostRequest')}\n${extractFunction(source, 'cactiPreparePostRequestFromUrl')}`, context);
test('state-changing URL fields move into a same-origin POST body with the trusted token', () => {
const request = context.cactiPreparePostRequestFromUrl('/cacti/cdef.php?action=item_remove&id=7&__csrf_magic=attacker');
const fields = new URLSearchParams(request.data);
assert.equal(request.url, '/cacti/cdef.php');
assert.equal(fields.get('action'), 'item_remove');
assert.equal(fields.get('id'), '7');
assert.equal(fields.get('__csrf_magic'), 'trusted-token');
});
test('an explicit POST body cannot override the trusted token', () => {
const request = context.cactiPreparePostRequest('/cacti/tree.php', 'action=tree_up&__csrf_magic=attacker&id=9');
const fields = new URLSearchParams(request.data);
assert.equal(fields.get('__csrf_magic'), 'trusted-token');
assert.equal(fields.get('action'), 'tree_up');
assert.equal(fields.get('id'), '9');
assert.doesNotMatch(request.data, /&&/);
});
test('cross-origin targets are rejected before a token is exposed', () => {
assert.throws(
() => context.cactiPreparePostRequestFromUrl('https://attacker.example/delete?action=item_remove'),
/different origin/,
);
});
test('the state-action handler survives element-level unbind calls', () => {
assert.match(source, /\$\(document\)\.rebind\('click\.cactiPostAction', 'a\.cactiPostAction'/);
assert.doesNotMatch(source, /\$\('a\.cactiPostAction'\)\.off\('click\.cactiPostAction'/);
});