diff --git a/Plugin/unraid/README-unraid.md b/Plugin/unraid/README-unraid.md index 57d2f2c..05b8747 100644 --- a/Plugin/unraid/README-unraid.md +++ b/Plugin/unraid/README-unraid.md @@ -114,34 +114,64 @@ be the reason a page fails to load. ## ━━━ WHAT GUARDS THE API LAYER ━━━ -Every endpoint under `api/` is protected by exactly one thing: **the Unraid WebGUI session.** -Anything that can reach `/plugins/varaverk/api/*.php` with a valid session can do everything -this plugin can do — stop the array, power off the host, write `master.conf` and push it to -every partner, create an lldap user, write and schedule a root-run script. +Two layers, both supplied by the platform, neither implemented in this plugin. -That is the same trust level as the rest of the WebGUI, and it is the intended model. It is -written down here because two things about it are easy to assume and wrong. +**1. nginx session auth.** `/etc/nginx/nginx.conf` applies `satisfy any; allow unix:; deny all; +auth_request /auth-request.php;` to everything it serves, `/plugins/` included. A request +without a valid WebGUI session never reaches PHP — it is redirected to the login page. -**No endpoint validates a CSRF token.** `pages/partnership.php` and `pages/scheduler.php` send -Unraid's `csrf_token` with their POSTs, which reads like the token is checked somewhere. It is -not — no file in `api/` looks at it. A request that arrives with a logged-in session cookie is -honoured whatever caused the browser to send it. Adding validation is a worthwhile hardening -pass, but it is a real change: every caller has to send the token before any endpoint requires -it, or the UI breaks silently on whichever page was missed. Do it deliberately, in one pass, -with the pages open — not opportunistically while touching one endpoint. +**2. Unraid's CSRF prepend.** `/etc/php.ini` sets +`auto_prepend_file=/usr/local/emhttp/webGui/include/local_prepend.php`, which runs before the +first line of any endpoint and, for **every POST**, requires a valid token as either a +`csrf_token` body field or an `X-CSRF-Token` header. On a mismatch it logs to syslog and +`exit`s. php-fpm inherits this — there is no pool override in `/etc/php-fpm.d/www.conf`. -**`api/webhook.php` is the exception that authenticates nothing at all.** It exists to receive -Sonarr/Radarr/Lidarr download events, which arrive from a container rather than a browser. -`master.conf` carries a `WEBHOOK_SECRET` and the standalone Node listener on `WEBHOOK_PORT` -validates it — this WebGUI-hosted path does not. Injection is not the risk (the path is -validated and escaped); triggering work is. Closing it means adding a secret check here **and** -updating the webhook URL in each arr's settings, in that order. +Anything that clears both can do everything this plugin can do: stop the array, power off the +host, write `master.conf` and push it to every partner, create an lldap user, write and +schedule a root-run script. That is the same trust level as the rest of the WebGUI, and it is +the intended model. -Two further endpoints are worth knowing about because they read wider than the rest: -`api/api_test.php` returns an API key prefix and the live GraphQL schema, and -`api/import_script.php`'s browse action and `api/manual_sync.php`'s browse actions list -directory names anywhere on either host. All three are read-only and none return file contents, -but they are the ones to look at first if the session boundary ever moves. +### The two consequences that actually matter + +**GET is not covered.** The prepend inspects POSTs only. So any action that *changes something* +must be POST — over GET it would run with no token check at all. Four actions were GET and have +been converted: `create_api_key.php`, `arrs.php?action=refresh_remote`, `cert.php?action=run` +and `setup.php?action=ssh_generate`. **When adding an endpoint the rule is simply: if it changes +state, it is POST.** Read-only GETs (`monitor`, `board`, `checklist`, the browse pickers, +`api_test.php`) are bounded by the session alone, which is correct for reads. + +**The webGUI's token injector is jQuery-only.** `$.ajaxPrefilter` in dynamix's +`HeadInlineJS.php` appends `csrf_token` to jQuery POSTs. Varaverk's pages use native `fetch()`, +which it does not touch — so a `fetch()` POST carries no token and the prepend kills it +silently: `csrf_terminate()` exits with an empty body, `r.json()` throws on the empty response, +and the page's own `.catch()` swallows it. There is no error anywhere except a syslog line. + +`Varaverk.page` therefore installs a small `window.fetch` shim, inline and above the tab +content, that attaches `X-CSRF-Token` to same-origin `/plugins/varaverk/` requests. The header +form is deliberate: it works for `FormData`, `URLSearchParams` and raw JSON bodies alike, so no +call site has to know about it and a new one cannot forget it. A cross-origin page cannot set +a custom header without a preflight it will fail, which is what makes it a real defence rather +than a formality. + +**Do not construct POSTs outside `fetch()`** — a raw `XMLHttpRequest` or a generated form — +without adding the token yourself; the shim only wraps `fetch`. A plain `