{"record":{"id":"be85bbbf971e5c22","repo":"getgrav/grav","slug":"invalid-username-contains-invalid-characters-or-s","errorCode":null,"errorMessage":"Invalid username: contains invalid characters or sequences","messagePattern":"Invalid username: contains invalid characters or sequences","errorType":"validation","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Common/User/DataUser/User.php","lineNumber":133,"sourceCode":"    /**\n     * Save user\n     *\n     * @return void\n     */\n    public function save()\n    {\n        /** @var CompiledYamlFile|null $file */\n        $file = $this->file();\n        if (!$file || !$file->filename()) {\n            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);\n        }\n\n        if ($file) {\n            $username = $this->filterUsername((string)$this->get('username'));\n\n            // Validate username to prevent path traversal attacks\n            if (!self::isValidUsername($username)) {\n                throw new \\RuntimeException('Invalid username: contains invalid characters or sequences');\n            }\n\n            if (!$file->filename()) {\n                $locator = Grav::instance()['locator'];\n\n                // Check if a user with this username already exists (prevent overwriting)\n                $existingFile = $locator->findResource('account://' . $username . YAML_EXT);\n                if ($existingFile) {\n                    throw new \\RuntimeException('User account with this username already exists');\n                }\n\n                $file->filename($locator->findResource('account://' . $username . YAML_EXT, true, true));\n            }\n\n            // if plain text password, hash it and remove plain text\n            $password = $this->get('password') ?? $this->get('password1');\n            if (null !== $password && '' !== $password) {\n                $password2 = $this->get('password2');","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Common/User/DataUser/User.php#L115-L151","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n$user = $grav['accounts']->load('');\n$user->set('username', $rawUsername);\n$user->save(); // RuntimeException for '../etc' style names\n\n// after\nif (!\\Grav\\Common\\User\\DataUser\\User::isValidUsername($rawUsername)) {\n    throw new \\InvalidArgumentException('Username contains invalid characters.');\n}\n$user->set('username', $rawUsername);\n$user->save();","handlingStrategy":"validation","validationCode":"use Grav\\Common\\User\\DataUser\\User as DataUser;\n$username = (string) $form->getValue('username');\nif (!DataUser::isValidUsername($username)) {\n    // reject: invalid characters, empty, leading dot, or '..'\n}","typeGuard":"use Grav\\Common\\User\\DataUser\\User as DataUser;\nfunction isAcceptableUsername(string $username): bool\n{\n    return (bool) preg_match('/^[A-Za-z0-9._-]{1,64}$/', $username)\n        && DataUser::isValidUsername($username);\n}","tryCatchPattern":"try {\n    $user->save();\n} catch (\\RuntimeException $e) {\n    if (str_starts_with($e->getMessage(), 'Invalid username')) {\n        // show a friendly 'choose a different username' error\n    }\n    throw $e;\n}","preventionTips":["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."],"tags":["user-management","security","path-traversal","validation","accounts"],"backgroundTag":"invalid-username-format","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}