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
- Restrict usernames at the form boundary (e.g. /^[A-Za-z0-9._-]+$/) before creating the account.
- Call DataUser\User::isValidUsername($username) yourself before save() so you can fail with a user-friendly message.
- 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
- Enforce a conservative username charset (letters, digits, dot, dash, underscore) in forms.
- Run DataUser::isValidUsername() in your own validation layer, not only at save time.
- Never build account file paths from raw input; always go through the accounts API.
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Passwords did not match.
- Passwords did not match.
- Backup location not allowed (outside site root): {$backup_ro
- ZipArchiver: refused to extract {archive_file}. Entry "{name
- User account with this username already exists
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/be85bbbf971e5c22.
Report an issue: GitHub.