getgrav/grav · error · RuntimeException

Invalid username: contains invalid characters or sequences

Error message

Invalid username: contains invalid characters or sequences

What it means

Before persisting a DataUser account, User::save() validates the username with isValidUsername() (User.php:350): it must be non-empty, must not contain \ / ? * : ; { } or newlines, must not contain '..', and must not start with a dot — because the username becomes the YAML filename under account://. Violations throw to block path traversal and filesystem abuse.

Source

Thrown at system/src/Grav/Common/User/DataUser/User.php:133

    /**
     * Save user
     *
     * @return void
     */
    public function save()
    {
        /** @var CompiledYamlFile|null $file */
        $file = $this->file();
        if (!$file || !$file->filename()) {
            user_error(self::class . ': calling \$user = new ' . self::class . "() is deprecated since Grav 1.6, use \$grav['accounts']->load(\$username) or \$grav['accounts']->load('') instead", E_USER_DEPRECATED);
        }

        if ($file) {
            $username = $this->filterUsername((string)$this->get('username'));

            // Validate username to prevent path traversal attacks
            if (!self::isValidUsername($username)) {
                throw new \RuntimeException('Invalid username: contains invalid characters or sequences');
            }

            if (!$file->filename()) {
                $locator = Grav::instance()['locator'];

                // Check if a user with this username already exists (prevent overwriting)
                $existingFile = $locator->findResource('account://' . $username . YAML_EXT);
                if ($existingFile) {
                    throw new \RuntimeException('User account with this username already exists');
                }

                $file->filename($locator->findResource('account://' . $username . YAML_EXT, true, true));
            }

            // if plain text password, hash it and remove plain text
            $password = $this->get('password') ?? $this->get('password1');
            if (null !== $password && '' !== $password) {
                $password2 = $this->get('password2');

View on GitHub (pinned to 6040efed04)

Solutions

  1. Restrict usernames at the form boundary (e.g. /^[A-Za-z0-9._-]+$/) before creating the account.
  2. Call DataUser\User::isValidUsername($username) yourself before save() so you can fail with a user-friendly message.
  3. For legacy usernames that cannot be changed, keep the display name in a separate field and derive a filesystem-safe account key.

Example fix

// before
$user = $grav['accounts']->load('');
$user->set('username', $rawUsername);
$user->save(); // RuntimeException for '../etc' style names

// after
if (!\Grav\Common\User\DataUser\User::isValidUsername($rawUsername)) {
    throw new \InvalidArgumentException('Username contains invalid characters.');
}
$user->set('username', $rawUsername);
$user->save();
Defensive patterns

Strategy: validation

Validate before calling

use Grav\Common\User\DataUser\User as DataUser;
$username = (string) $form->getValue('username');
if (!DataUser::isValidUsername($username)) {
    // reject: invalid characters, empty, leading dot, or '..'
}

Type guard

use Grav\Common\User\DataUser\User as DataUser;
function isAcceptableUsername(string $username): bool
{
    return (bool) preg_match('/^[A-Za-z0-9._-]{1,64}$/', $username)
        && DataUser::isValidUsername($username);
}

Try / catch

try {
    $user->save();
} catch (\RuntimeException $e) {
    if (str_starts_with($e->getMessage(), 'Invalid username')) {
        // show a friendly 'choose a different username' error
    }
    throw $e;
}

Prevention

When it happens

Trigger: Saving an account whose username is empty, contains a slash (e.g. 'domain/user'), starts with '.' (hidden file), embeds '..' (traversal payload), or includes reserved characters like ':'; a form submitting username as an array so (string) cast yields something invalid; migration scripts importing usernames verbatim from another system.

Common situations: Migrating users from systems allowing colons/backslashes or email-style usernames with path characters; custom registration forms lacking username validation; security probes posting traversal payloads to account-creation endpoints.

Understand the failure class

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/be85bbbf971e5c22. Report an issue: GitHub.