Skip to content

Commit 3d419a0

Browse files
committed
Security: registration privilege strip + Grav 2.0 nonce-key bridge
- GHSA-pxm6-mhxr-q4mj (critical, unauth): the registration form handler now ignores client-supplied `groups` and `access` even if an admin added them to user_registration.fields, preventing self-registration as super-admin. Logs a warning on any attempted injection. Server-side default_values, invitations and the user_registration.{groups,access} config remain authoritative. - IP pseudonymization (rate-limit keys, remember-me cookie salt) now uses Security::getNonceKey() when running on Grav 2.0+, falling back to the legacy security.salt config read on Grav 1.7. Tracks Grav 2.0's GHSA-3f29-pqwf-v4j4 fix without breaking 1.7 compatibility.
1 parent f00fad1 commit 3d419a0

3 files changed

Lines changed: 40 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# v3.8.2
2+
## 04/23/2026
3+
4+
1. [](#bugfix)
5+
* [security] Fixed unauthenticated privilege escalation in `Login::register` (GHSA-pxm6-mhxr-q4mj): if an admin added `groups` or `access` to `user_registration.fields`, an attacker could self-register as super-admin by POSTing those values. Registration form input for those fields is now ignored with a log warning; server-side config, `default_values`, and invitations remain authoritative.
6+
2. [](#improved)
7+
* IP pseudonymization (rate-limit keys, remember-me cookie salt) now uses `Security::getNonceKey()` when running on Grav 2.0+, and continues to read `security.salt` from config on Grav 1.7. Tracks Grav 2.0's GHSA-3f29-pqwf-v4j4 remediation (HMAC key is no longer reachable via sandboxed Twig).
8+
19
# v3.8.1
210
## 04/17/2026
311

classes/Login.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Grav\Common\Page\Interfaces\PageInterface;
2020
use Grav\Common\Page\Page;
2121
use Grav\Common\Page\Pages;
22+
use Grav\Common\Security;
2223
use Grav\Common\Session;
2324
use Grav\Common\User\Interfaces\UserCollectionInterface;
2425
use Grav\Common\User\Interfaces\UserInterface;
@@ -349,8 +350,13 @@ public function getIpKey(?string $ip = null): string
349350
$isIPv4 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
350351
$ipKey = $isIPv4 ? $ip : Utils::getSubnet($ip, $this->grav['config']->get('plugins.login.ipv6_subnet_size'));
351352

352-
// Pseudonymization of the IP
353-
return sha1($ipKey . $this->grav['config']->get('security.salt'));
353+
// Pseudonymization of the IP. Grav 2.0 moved the HMAC key out of Config
354+
// (GHSA-3f29-pqwf-v4j4); fall back to the legacy key on Grav 1.7.
355+
$key = method_exists(Security::class, 'getNonceKey')
356+
? Security::getNonceKey()
357+
: (string) $this->grav['config']->get('security.salt');
358+
359+
return sha1($ipKey . $key);
354360
}
355361

356362
/**
@@ -581,9 +587,13 @@ public function rememberMe($var = null)
581587
$this->rememberMe->setExpireTime($timeout);
582588

583589
// Hardening cookies with user-agent and random salt or
584-
// fallback to use system based cache key
590+
// fallback to use system based cache key. Grav 2.0 moved the HMAC key
591+
// out of Config (GHSA-3f29-pqwf-v4j4); use it when available.
585592
$server_agent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
586-
$data = $server_agent . $config->get('security.salt', $this->grav['cache']->getKey());
593+
$saltKey = method_exists(Security::class, 'getNonceKey')
594+
? Security::getNonceKey()
595+
: (string) $config->get('security.salt', $this->grav['cache']->getKey());
596+
$data = $server_agent . $saltKey;
587597
$this->rememberMe->setSalt(hash('sha512', $data));
588598

589599
// Set cookie with correct base path of Grav install

login.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,13 @@ private function processUserRegistration(FormInterface $form, Event $event): voi
10231023

10241024
$fields = (array)$this->config->get('plugins.login.user_registration.fields', []);
10251025

1026+
// Privilege fields must never be sourced from public registration form input —
1027+
// honoring attacker-supplied values would grant instant super-admin even if an
1028+
// administrator mistakenly added them to `user_registration.fields`
1029+
// (GHSA-pxm6-mhxr-q4mj). Server-side `default_values`, invitations, and the
1030+
// `plugins.login.user_registration.{groups,access}` config remain authoritative.
1031+
$privilegeFields = ['groups', 'access'];
1032+
10261033
foreach ($fields as $field) {
10271034
// Process value of field if set in the page process.register_user
10281035
$default_values = (array)$this->config->get('plugins.login.user_registration.default_values');
@@ -1040,6 +1047,17 @@ private function processUserRegistration(FormInterface $form, Event $event): voi
10401047
}
10411048
}
10421049

1050+
if (in_array($field, $privilegeFields, true)) {
1051+
if ($form_data->get($field) !== null) {
1052+
$this->grav['log']->warning(sprintf(
1053+
'Login registration: ignored client-supplied "%s" from form submission (username=%s)',
1054+
$field,
1055+
is_string($username) ? $username : '<invalid>'
1056+
));
1057+
}
1058+
continue;
1059+
}
1060+
10431061
if (!isset($data[$field]) && $form_data->get($field)) {
10441062
$data[$field] = $form_data->get($field);
10451063
}

0 commit comments

Comments
 (0)