GitHub Security Advisory: GHSA-pxm6-mhxr-q4mj
Package: getgrav/grav (COMPOSER)
Severity: CRITICAL (CVSS 9.4)
CWE: CWE-20: Improper Input Validation
Vulnerable versions: < 2.0.0-beta.2
Patched in: 2.0.0-beta.2

Summary: Grav Vulnerable to Privilege Escalation via Missing Server-Side Validation of groups/access

Details:
# Bug Report: Registration Privilege Escalation via Missing Server-Side Validation of groups/access

## Summary

The `Login::register()` method in the Login plugin accepts attacker-controlled `groups` and `access` fields from the registration POST data without server-side validation. When registration is enabled and `groups` or `access` are included in the configured allowed fields list, an unauthenticated user can self-register with `admin.super` privileges by injecting these fields into the registration request.

This is a missing server-side validation issue — the only defense is a config-level `fields` allowlist, which is an admin-facing setting, not a hardcoded security boundary.

## Affected Component

- **File:** `user/plugins/login/classes/Login.php`, lines 246-306
- **Method:** `Login::register()`
- **Validation:** `Login::validateField()`, lines 363-432
- **Plugin:** Login Plugin 3.8.0
- **Grav:** 1.8.0-beta.29

## Root Cause

In `register()` (lines 254-267), the `groups` and `access` fields are only set to config defaults **if they are not already present in the input data**:

```php
// Line 254-260
if (!isset($data['groups'])) {
    $groups = (array) $this->config->get('plugins.login.user_registration.groups', []);
    if (count($groups) > 0) {
        $data['groups'] = $groups;
    }
}

// Line 262-267
if (!isset($data['access'])) {
    $access = (array) $this->config->get('plugins.login.user_registration.access.site', []);
    if (count($access) > 0) {
        $data['access']['site'] = $access;
    }
}
```

If an attacker **includes** `groups` or `access` in the POST body, the `!isset()` check passes and the config defaults are skipped. The attacker's values flow through unchanged.

Later (lines 298-303), these values are assigned directly to the user object:

```php
if (isset($data['groups'])) {
    $user->groups = $data['groups'];  // attacker-controlled
}
if (isset($data['access'])) {
    $user->access = $data['access'];  // attacker-controlled
}
$user->save();
```

The `validateField()` method (lines 363-432) has a `switch` statement that only validates: `username`, `password`, `password2`, `email`, `permissions`, `state`, and `language`. The `groups` and `access` fields pass through the `default` case with **no validation at all**.

## Precondition

Registration must be enabled with `groups` and/or `access` in the configured allowed fields:

```yaml
# user/config/plugins/login.yaml
user_registration:
  enabled: true
  fields:
    - username
    - password
    - email
    - fullname
    - groups    # ← enables the attack
    - access    # ← enables the attack
```

This is a configuration the admin UI allows without any warning. An admin adding `groups` to let users pick a non-privileged group (e.g., `editors`) unknowingly exposes the escalation path, since there is no validation constraining which groups can be selected.

## Proof of Concept

### Malicious registration request (unauthenticated):

```bash
curl -X POST "${TARGET}/