Carry the CSRF token on fetch requests and put mutations behind POST
Unraid already enforces CSRF on every POST via auto_prepend, but its injector is jQuery-only — the plugin's native fetch() calls carried no token and were being terminated before the endpoint ran, silently, because csrf_terminate exits with an empty body that r.json() swallows.
This commit is contained in:
@@ -8,7 +8,55 @@ $docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
|
||||
$pluginDir = "$docroot/plugins/$plugin";
|
||||
|
||||
require_once "$pluginDir/include/config.php";
|
||||
?>
|
||||
|
||||
<script>
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// CSRF token propagation — must run before any page JS.
|
||||
//
|
||||
// Unraid enforces CSRF centrally: /etc/php.ini sets auto_prepend_file to webGui's
|
||||
// local_prepend.php, which terminates *every* POST that does not carry a valid token, before a
|
||||
// single line of endpoint code runs. It accepts the token as a `csrf_token` POST field or as an
|
||||
// X-CSRF-Token header.
|
||||
//
|
||||
// The webGUI's own injector is jQuery-only ($.ajaxPrefilter in HeadInlineJS.php). Varaverk's
|
||||
// pages use native fetch(), which that prefilter does not touch — so without this shim every
|
||||
// mutating request in the plugin is silently killed by the platform. Silently, because
|
||||
// csrf_terminate() exits with no body: the fetch resolves, r.json() throws on the empty
|
||||
// response, and the page's own .catch() swallows it.
|
||||
//
|
||||
// Setting the header rather than appending a body field is deliberate. It works identically for
|
||||
// FormData, URLSearchParams and raw JSON bodies, so no call site has to know about it and a new
|
||||
// endpoint cannot forget to include it. The header is only ever attached to same-origin
|
||||
// Varaverk URLs; a cross-origin page cannot set a custom header without a preflight it will
|
||||
// fail, which is precisely what makes this a CSRF defence rather than a formality.
|
||||
//
|
||||
// Inline, and above the tab content, because pages/*.php carry their own inline fetch calls and
|
||||
// some fire on load — an external script could not be guaranteed to install first.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
(function () {
|
||||
if (window.__vvCsrfInstalled) return;
|
||||
window.__vvCsrfInstalled = true;
|
||||
|
||||
var nativeFetch = window.fetch.bind(window);
|
||||
|
||||
window.fetch = function (input, init) {
|
||||
var url = (typeof input === 'string') ? input : (input && input.url) || '';
|
||||
|
||||
if (url.indexOf('/plugins/varaverk/') !== -1 &&
|
||||
typeof csrf_token !== 'undefined' && csrf_token) {
|
||||
init = init || {};
|
||||
var headers = new Headers(init.headers || (typeof input === 'object' && input.headers) || {});
|
||||
if (!headers.has('X-CSRF-Token')) headers.set('X-CSRF-Token', csrf_token);
|
||||
init = Object.assign({}, init, { headers: headers });
|
||||
}
|
||||
|
||||
return nativeFetch(input, init);
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
// First-run check — show setup wizard if HOST1 is blank OR local host.conf is missing
|
||||
$_master = vv_read_conf_raw('master.conf');
|
||||
preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $_master, $_h1m);
|
||||
|
||||
Reference in New Issue
Block a user