From 71d10f8cf6a73ad9f3f7e01a9933bd4ceffc5dcd Mon Sep 17 00:00:00 2001 From: the-djmaze <3752035+the-djmaze@users.noreply.github.com> Date: Mon, 2 Nov 2026 01:07:33 +0100 Subject: [PATCH] fix(security): require CSRF token for headerless GET JSON actions https://github.com/the-djmaze/snappymail/commit/ba3741080a9ba68e8fa6cefbbd35254788de7175 --- .../0.0.0/app/libraries/RainLoop/ServiceActions.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php index b3ad82241..a1bc2637e 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php @@ -103,11 +103,17 @@ class ServiceActions $this->oActions->logWrite("{$_SERVER['HTTP_X_SM_TOKEN']} !== {$token} for {$sEmail}", \LOG_ERR, 'Token'); throw new Exceptions\ClientException(Notifications::InvalidToken, null, 'HTTP Token mismatch'); } - } else if ($this->oHttp->IsPost()) { - if (empty($_POST['XToken']) || $_POST['XToken'] !== $token) { + } else { + // A state-changing action must never be triggerable by a bare + // headerless POST/GET (e.g. or a top-level navigation): + // without the X-SM-Token header the token has to arrive as a + // query parameter. The first-party UI always sends the header, + // so this only affects headerless callers. + $xToken = $this->oHttp->IsPost() ? ($_POST['XToken'] ?? '') : ($_GET['XToken'] ?? ''); + if (empty($xToken) || $xToken !== $token) { $oAccount = $this->oActions->getAccountFromToken(false); $sEmail = $oAccount ? $oAccount->Email() : 'guest'; - $this->oActions->logWrite("{$_POST['XToken']} !== {$token} for {$sEmail}", \LOG_ERR, 'XToken'); + $this->oActions->logWrite("{$xToken} !== {$token} for {$sEmail}", \LOG_ERR, 'XToken'); throw new Exceptions\ClientException(Notifications::InvalidToken, null, 'XToken mismatch'); } }