mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
* 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
81 lines
3.3 KiB
JavaScript
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'/);
|
|
});
|