mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-30 16:47:14 +09:00
* build: add symfony/security-csrf Backs the CSRF token core that replaces the vendored csrf-magic fork. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: fix security-csrf package path assertion symfony/security-csrf ships CsrfTokenManager.php at the package root, not under src/. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * refactor: register the composer autoloader before the CSRF include CSRF validation ran before the autoloader existed, which blocks any class-based guard. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * feat(csrf): add CactiCsrfGuard over symfony/security-csrf Issues and validates session-scoped tokens; not yet wired into the request path. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * feat(csrf): accept pre-upgrade csrf-magic tokens during the grace window Legacy tokens carry their own expiry, so the fallback closes itself without a config flag. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * feat(csrf): port the output buffer rewriter to CactiCsrfGuard Keeps the hidden field name and JS globals that third-party plugins rely on. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(csrf): use json_encode for the JS script context in rewriteBuffer htmlspecialchars() protects the HTML attribute context but not the <script> string literal, which relied on Symfony's token generator never emitting a quote rather than being safe by construction. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * refactor(csrf): move the XHR shim to include/js as Cacti-owned code It has been maintained by the Cacti Group since 2020 and is not an upstream package. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * refactor(csrf): retire the vendored csrf-magic fork The procedural csrf_* surface stays as a deprecated shim so plugins keep working. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * feat(csrf): validate requests through CactiCsrfGuard csrf-magic self-invoked from its own file; the guard is called explicitly. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(install): stop gating the permission step on the csrf secret Retiring csrf-magic removed the directory its default path lives in, and setStep() clamps to the first errored step, so no fresh install could advance. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(csrf): key the legacy fallback on the raw secret bytes refresh_csrf.php writes PHP source with a trailing newline, and trimming that yields a different HMAC key than csrf-magic used. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(csrf): log when the guard degrades to disabled in a web context The installer is the only web path that can reach it, and failing open in silence is not something csrf-magic could do. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs: changelog for the CSRF library replacement Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(csrf): mint the token before the session write callback closes it csrf_startup() minted lazily from the output buffer callback, which runs after session_write_close()'s shutdown-function registration, so a token minted there on a legacy-fallback POST was silently dropped. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(csrf): make refresh_csrf.php a no-op Its default secret path no longer ships and live tokens are session-bound, not HMACs over the secret, so rotation neither worked nor did anything meaningful; running it during the upgrade grace window used to delete the legacy secret and kill validation for pre-upgrade tokens outright. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(csrf): fall back to the old default secret path when reading it csrf-magic tried its own directory as a second candidate regardless of $path_csrf_secret; the new reader only tried the configured path, so a site pointing $path_csrf_secret at a file that was never created lost the legacy-token grace window entirely. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs(csrf): point the JS header at its BSD license sidecar The header reduces the original BSD terms to prose and the actual text sits in an unreferenced csrf.js.LICENSE, so add a pointer to it. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(csrf): restore csrf_check()'s bool return and $fatal contract The shim ignored $fatal and always returned void, so audit/audit.php's `!csrf_check(false)` evaluated `!null` (always true) and denied every request instead of getting a real pass/fail result back. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs: reference the pull request number in the changelog entry Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: resolve includes through Cacti path constants Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(csrf): enforce full coverage Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Harden CSRF fallback handling Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(csrf): assert the component stays out of the index Committing it would leave a second copy to drift against the lock. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: align CSRF checks with PHP 8.2 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs: normalize the CSRF changelog entry * Fix CHANGELOG entry ordering Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(csrf): support constrained test environments * fix(csrf): narrow nullable guard state * test: place CSRF coverage in security domain Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * security(test): match disabled exec exactly Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: run CSRF coverage on PHP 8.3 Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <thewitness@cacti.net>
223 lines
7.8 KiB
JavaScript
223 lines
7.8 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. |
|
|
| |
|
|
| 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 |
|
|
+-------------------------------------------------------------------------+
|
|
| Derived from csrf-magic by Edward Z. Yang, BSD licensed. Maintained |
|
|
| by the Cacti Group since 2020; upstream has been inactive since 2016. |
|
|
| See include/js/csrf.js.LICENSE for the original BSD terms. |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
/**
|
|
* @file
|
|
*
|
|
* Rewrites XMLHttpRequest to automatically send CSRF token with it. In theory
|
|
* plays nice with other JavaScript libraries, needs testing though.
|
|
*/
|
|
|
|
// Here are the basic overloaded method definitions
|
|
// The wrapper must be set BEFORE onreadystatechange is written to, since
|
|
// a bug in ActiveXObject prevents us from properly testing for it.
|
|
CsrfMagic = function(real) {
|
|
// try to make it ourselves, if you didn't pass it
|
|
if (!real) try { real = new XMLHttpRequest; } catch (e) {;}
|
|
if (!real) try { real = new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) {;}
|
|
if (!real) try { real = new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) {;}
|
|
if (!real) try { real = new ActiveXObject('Msxml2.XMLHTTP.4.0'); } catch (e) {;}
|
|
this.csrf = real;
|
|
// properties
|
|
var csrfMagic = this;
|
|
real.onreadystatechange = function() {
|
|
csrfMagic._updateProps();
|
|
return csrfMagic.onreadystatechange ? csrfMagic.onreadystatechange() : null;
|
|
};
|
|
csrfMagic._updateProps();
|
|
}
|
|
|
|
CsrfMagic.prototype = {
|
|
|
|
open: function(method, url, async, username, password) {
|
|
if (method == 'POST') this.csrf_isPost = true;
|
|
// deal with Opera bug, thanks jQuery
|
|
if (username) return this.csrf_open(method, url, async, username, password);
|
|
else return this.csrf_open(method, url, async);
|
|
},
|
|
csrf_open: function(method, url, async, username, password) {
|
|
if (username) return this.csrf.open(method, url, async, username, password);
|
|
else return this.csrf.open(method, url, async);
|
|
},
|
|
|
|
send: function(data) {
|
|
if (!this.csrf_isPost) return this.csrf_send(data);
|
|
prepend = csrfMagicName + '=' + csrfMagicToken + '&';
|
|
delete this.csrf_isPost;
|
|
|
|
if (typeof data == 'object') {
|
|
prepend = data;
|
|
prepend[csrfMagicName] = csrfMagicToken;
|
|
} else {
|
|
prepend = csrfMagicName + '=' + csrfMagicToken;
|
|
if (data) prepend = prepend + '&' + data;
|
|
}
|
|
return this.csrf_send(prepend);
|
|
},
|
|
csrf_send: function(data) {
|
|
return this.csrf.send(data);
|
|
},
|
|
|
|
setRequestHeader: function(header, value) {
|
|
// We have to auto-set this at the end, since we don't know how long the
|
|
// nonce is when added to the data.
|
|
if (this.csrf_isPost && header == "Content-length") {
|
|
this.csrf_purportedLength = value;
|
|
return;
|
|
}
|
|
return this.csrf_setRequestHeader(header, value);
|
|
},
|
|
csrf_setRequestHeader: function(header, value) {
|
|
return this.csrf.setRequestHeader(header, value);
|
|
},
|
|
|
|
abort: function() {
|
|
return this.csrf.abort();
|
|
},
|
|
getAllResponseHeaders: function() {
|
|
return this.csrf.getAllResponseHeaders();
|
|
},
|
|
getResponseHeader: function(header) {
|
|
return this.csrf.getResponseHeader(header);
|
|
} // ,
|
|
}
|
|
|
|
// proprietary
|
|
CsrfMagic.prototype._updateProps = function() {
|
|
this.readyState = this.csrf.readyState;
|
|
if (this.readyState == 4) {
|
|
this.responseText = this.csrf.responseText;
|
|
this.responseXML = this.csrf.responseXML;
|
|
this.status = this.csrf.status;
|
|
this.statusText = this.csrf.statusText;
|
|
}
|
|
}
|
|
CsrfMagic.process = function(base) {
|
|
if (typeof base == 'object') {
|
|
prepend = base;
|
|
prepend[csrfMagicName] = csrfMagicToken;
|
|
} else {
|
|
var prepend = csrfMagicName + '=' + csrfMagicToken;
|
|
if (base) prepend = prepend + '&' + base;
|
|
}
|
|
return prepend;
|
|
}
|
|
// callback function for when everything on the page has loaded
|
|
CsrfMagic.end = function() {
|
|
// This rewrites forms AGAIN, so in case buffering didn't work this
|
|
// certainly will.
|
|
forms = document.getElementsByTagName('form');
|
|
for (var i = 0; i < forms.length; i++) {
|
|
form = forms[i];
|
|
|
|
if (typeof form.method.toUpperCase == 'function' && form.method.toUpperCase() !== 'POST') {
|
|
continue;
|
|
}
|
|
|
|
if (form.elements[csrfMagicName]) continue;
|
|
var input = document.createElement('input');
|
|
input.setAttribute('name', csrfMagicName);
|
|
input.setAttribute('value', csrfMagicToken);
|
|
input.setAttribute('type', 'hidden');
|
|
form.appendChild(input);
|
|
}
|
|
}
|
|
|
|
// Sets things up for Mozilla/Opera/nice browsers
|
|
// We very specifically match against Internet Explorer, since they haven't
|
|
// implemented prototypes correctly yet.
|
|
if (window.XMLHttpRequest && window.XMLHttpRequest.prototype && '\v' != 'v') {
|
|
var x = XMLHttpRequest.prototype;
|
|
var c = CsrfMagic.prototype;
|
|
|
|
// Save the original functions
|
|
x.csrf_open = x.open;
|
|
x.csrf_send = x.send;
|
|
x.csrf_setRequestHeader = x.setRequestHeader;
|
|
|
|
// Notice that CsrfMagic is itself an instantiatable object, but only
|
|
// open, send and setRequestHeader are necessary as decorators.
|
|
x.open = c.open;
|
|
x.send = c.send;
|
|
x.setRequestHeader = c.setRequestHeader;
|
|
} else {
|
|
// The only way we can do this is by modifying a library you have been
|
|
// using. We support YUI, script.aculo.us, prototype, MooTools,
|
|
// jQuery, Ext and Dojo.
|
|
if (window.jQuery) {
|
|
// jQuery didn't implement a new XMLHttpRequest function, so we have
|
|
// to do this the hard way.
|
|
jQuery.csrf_ajax = jQuery.ajax;
|
|
jQuery.ajax = function( s ) {
|
|
if (s.type && s.type.toUpperCase() == 'POST') {
|
|
s = jQuery.extend(true, s, jQuery.extend(true, {}, jQuery.ajaxSettings, s));
|
|
if ( s.data && s.processData && typeof s.data != "string" ) {
|
|
s.data = jQuery.param(s.data);
|
|
}
|
|
s.data = CsrfMagic.process(s.data);
|
|
}
|
|
return jQuery.csrf_ajax( s );
|
|
}
|
|
}
|
|
if (window.Prototype) {
|
|
// This works for script.aculo.us too
|
|
Ajax.csrf_getTransport = Ajax.getTransport;
|
|
Ajax.getTransport = function() {
|
|
return new CsrfMagic(Ajax.csrf_getTransport());
|
|
}
|
|
}
|
|
if (window.MooTools) {
|
|
Browser.csrf_Request = Browser.Request;
|
|
Browser.Request = function () {
|
|
return new CsrfMagic(Browser.csrf_Request());
|
|
}
|
|
}
|
|
if (window.YAHOO) {
|
|
// old YUI API
|
|
YAHOO.util.Connect.csrf_createXhrObject = YAHOO.util.Connect.createXhrObject;
|
|
YAHOO.util.Connect.createXhrObject = function (transaction) {
|
|
obj = YAHOO.util.Connect.csrf_createXhrObject(transaction);
|
|
obj.conn = new CsrfMagic(obj.conn);
|
|
return obj;
|
|
}
|
|
}
|
|
if (window.Ext) {
|
|
// Ext can use other js libraries as loaders, so it has to come last
|
|
// Ext's implementation is pretty identical to Yahoo's, but we duplicate
|
|
// it for comprehensiveness's sake.
|
|
Ext.lib.Ajax.csrf_createXhrObject = Ext.lib.Ajax.createXhrObject;
|
|
Ext.lib.Ajax.createXhrObject = function (transaction) {
|
|
obj = Ext.lib.Ajax.csrf_createXhrObject(transaction);
|
|
obj.conn = new CsrfMagic(obj.conn);
|
|
return obj;
|
|
}
|
|
}
|
|
if (window.dojo) {
|
|
// NOTE: this doesn't work with latest dojo
|
|
dojo.csrf__xhrObj = dojo._xhrObj;
|
|
dojo._xhrObj = function () {
|
|
return new CsrfMagic(dojo.csrf__xhrObj());
|
|
}
|
|
}
|
|
}
|
|
|