{"record":{"id":"fd90c24ada0457f9","repo":"getgrav/grav","slug":"passwords-did-not-match-fd90c2","errorCode":null,"errorMessage":"Passwords did not match.","messagePattern":"Passwords did not match\\.","errorType":"validation","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Common/User/DataUser/User.php","lineNumber":153,"sourceCode":"\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');\n                if (!\\is_string($password) || ($password2 && $password !== $password2)) {\n                    throw new \\RuntimeException('Passwords did not match.');\n                }\n\n                $this->set('hashed_password', Authentication::create($password));\n            }\n            $this->undef('password');\n            $this->undef('password1');\n            $this->undef('password2');\n\n            $data = $this->items;\n            if ($username === $data['username']) {\n                unset($data['username']);\n            }\n            unset($data['authenticated'], $data['authorized']);\n\n            $file->save($data);\n\n            // We need to signal Flex Users about the change.\n            /** @var Flex|null $flex */","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Common/User/DataUser/User.php#L135-L171","documentation":"The DataUser (non-flex, YAML file) save() path in system/src/Grav/Common/User/DataUser/User.php:153 performs the same password confirmation as the Flex implementation: a non-empty 'password'/'password1' that is not a string, or that differs from a non-empty 'password2', throws before the hash is written with Authentication::create(). Empty password2 skips the comparison, so the error always involves a present-but-mismatched confirmation (or a non-string password).","triggerScenarios":"Calling save() on a DataUser account after set('password', ...) and set('password2', ...) with different non-empty values; posting a form whose password field arrives as an array; copying password1 into the object while leaving a stale password2 in the same payload.","commonSituations":"Legacy sites using data accounts (system.accounts not set to flex) with custom registration forms; profile-save endpoints that forward whole request bodies; double submission making the confirmation field diverge from the password field.","solutions":["Ensure password and password2 are identical non-empty strings before save(), or leave password2 unset to skip the check.","Reject array/non-scalar password values at the boundary before they reach the account object.","Catch RuntimeException around save() and re-present the form with a 'passwords did not match' error."],"exampleFix":"// before\n$user->set('password', $data['password']);\n$user->set('password2', $data['password2']);\n$user->save(); // RuntimeException('Passwords did not match.')\n\n// after\nif (!\\is_string($data['password']) || ($data['password2'] ?? '') !== $data['password']) {\n    throw new \\InvalidArgumentException('Passwords did not match.');\n}\n$user->set('password', $data['password']);\n$user->save(); // password2 not set — comparison skipped","handlingStrategy":"validation","validationCode":"$pass = $data['password'] ?? $data['password1'] ?? null;\n$confirm = $data['password2'] ?? null;\nif (null !== $pass && '' !== $pass) {\n    if (!\\is_string($pass) || ($confirm !== null && $confirm !== '' && $confirm !== $pass)) {\n        // reject before save()\n    }\n}","typeGuard":"function passwordsAgree(mixed $pass, mixed $confirm): bool\n{\n    return null === $pass || '' === $pass\n        || (\\is_string($pass) && (!$confirm || $confirm === $pass));\n}","tryCatchPattern":"try {\n    $user->save();\n} catch (\\RuntimeException $e) {\n    if (str_contains($e->getMessage(), 'Passwords did not match')) {\n        // re-show the password form\n    }\n    throw $e;\n}","preventionTips":["Validate password/password2 pairs in your form layer before touching the account object.","Compare, then undef password2 so no stale confirmation reaches save().","Reject non-scalar password inputs at the request boundary."],"tags":["user-management","password","validation","accounts","data-user"],"backgroundTag":"password-confirmation-mismatch","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}