Protection CSRF (Origin + jeton scopé)
Why protect POST endpoints
Without a session-bound CSRF token, a third-party site can submit a POST to PmaControl while an authenticated user is browsing elsewhere. The browser automatically attaches the session cookies and the action runs without user intent — configuration change, operation trigger, inline update.
Shared helper in Glial
From Glial v5.1.40 onwards, CSRF logic and Origin/Referer validation live in the framework and are shared by every PmaControl controller:
Glial\Security\Csrf::issueToken()/Csrf::validateToken()— issuance and validation of a token per functional scope.Glial\Http\Request::isSameSite()— compares Origin (priority) then Referer (fallback) against the current origin.
Worker.php only contains the HTTP orchestration specific to POST /Worker/update.Flow diagram
Token issuance
Before rendering a view that contains a form or an inline-editable cell, the controller issues a token scoped by feature:
<?php
use Glial\Security\Csrf;
$token = Csrf::issueToken($_SESSION, 'worker.update');
$this->set('csrf_token', $token);
worker.update from being replayed on daemon.start. The token is stored in $_SESSION['csrf_tokens'][$scope] and persists until the end of the user session.Client-side injection
For inline-editable cells (bootstrap-editable), the token is injected via two data-* attributes:
<td class="editable"
data-name="nb_worker"
data-csrf-field="csrf_token"
data-csrf-token="<?= htmlspecialchars($csrf_token, ENT_QUOTES, 'UTF-8') ?>">
5
</td>
htmlspecialchars(..., ENT_QUOTES, 'UTF-8') to neutralise any HTML/JS injection inside the cell.The App/Webroot/js/Tree/index.js JS detects the presence of data-csrf-token and automatically appends the token to the POST parameters sent by bootstrap-editable:
$.fn.editable.defaults.params = function (params) {
var $cell = $(this).closest('[data-csrf-token]');
if ($cell.length) {
params[$cell.data('csrf-field')] = $cell.data('csrf-token');
}
return params;
};
window.pmacontrolInitLineEdit(context) is called again on the reloaded context to bind the new token to the new cells.Server-side validation chain
Four checks are chained in this exact order, before any SQL is built:
- HTTP method — must be
POST; any other method returns405 Method Not Allowed. - Same-site origin —
Originfirst, thenRefereras fallback;nullvalues, protocol-relative URLs and any different scheme/host/port are rejected with403 Invalid request origin. - CSRF token —
hash_equals()comparison between thecsrf_tokenpayload field and$_SESSION['csrf_tokens'][$scope]; a missing or invalid token returns403 Invalid CSRF token. - Payload allowlist — only the fields and types expected for this endpoint; an out-of-list payload returns
400 Invalid worker update payload.
<?php
class Worker
{
public function update()
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
return;
}
if (!Glial\Http\Request::isSameSite($_SERVER)) {
http_response_code(403);
echo 'Invalid request origin';
return;
}
if (!Glial\Security\Csrf::validateToken($_POST, $_SESSION, 'worker.update')) {
http_response_code(403);
echo 'Invalid CSRF token';
return;
}
$sql = $this->buildWorkerUpdateSql($_POST);
if ($sql === null) {
http_response_code(400);
echo 'Invalid worker update payload';
return;
}
// SQL exécuté ici uniquement
}
}
Reference — return codes
| Code | Reason | When |
|---|---|---|
405 |
Method Not Allowed | HTTP method other than POST. |
403 |
Invalid request origin | Off-site Origin/Referer, Origin: null, protocol-relative URL, or different scheme/host/port. |
403 |
Invalid CSRF token | Token missing from payload or not matching $_SESSION['csrf_tokens'][$scope]. |
400 |
Invalid worker update payload | Field out of allowlist, non-integer value, negative or zero pk. |
Documented exemptions
The CSRF helper covers web POST endpoints only. The following surfaces are explicitly exempted:
- CLI
php glial …— no session cookie, no CSRF surface. - Machine-to-machine REST API — authenticated by a separate API token; the Origin/Referer check is disabled for these routes and the exemption is documented per endpoint.
- Read-only endpoints — converted to GET, therefore outside the CSRF perimeter.
Dedicated tests
CSRF protection is covered by PHPUnit tests that explicitly materialise the historical attack scenario:
./vendor/bin/phpunit tests/Glial/Security/CsrfTest.php
./vendor/bin/phpunit tests/Glial/Http/RequestTest.php
./vendor/bin/phpunit tests/Controller/WorkerUpdateSecurityTest.php
Origin: https://attacker.test or any other external site, the payload is rejected with 403 Invalid request origin before any write, even when a valid CSRF token is provided — the validation chain stops the request on the Origin check before reaching the token.