No step-up is an account-takeover primitive
A password or email change that accepts only a bearer token, with no current password and no fresh-auth check, is an ATO primitive on its own. Here is how I test for it and why "you need the token" is a weak defence.
Most people file “change password has no current-password field” as informational. That is the wrong instinct. The missing re-check is not a hardening nit, it is the last gate between a leaked or borrowed token and a permanent takeover. Treat it as a primitive and the severity argument writes itself.
The shape of the request
Here is the endpoint that should make you stop. A change-email handler, sanitised:
POST /api/v1/account/email HTTP/1.1
Host: app.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Content-Type: application/json
{"new_email":"attacker@example.com"}Notice what is not in that body. No current_password. No recently issued
re-auth token. No challenge. The server reads the subject from the JWT, updates
the record, and (if you are unlucky) does not even send a confirmation to the old
address. Whoever holds a valid bearer at request time owns the account from that
point on, because the recovery address is now theirs.
Password change is the same story:
POST /api/v1/account/password HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
{"new_password":"attacker-controlled"}If that succeeds without proving the current password or a fresh login, every other session is irrelevant, the attacker just sets a credential they know.
Why “but you need the token” is a weak mitigation
The reflexive defence from engineers is that you already have to be the user to make the request. That argument leaks for three concrete reasons, and I name all three in a report so triage cannot wave it away.
The through-line: a bearer proves you made a request, it does not prove you are present and intentional right now. Sensitive actions need the second thing.
What a correct gate looks like
Three mechanisms, in rough order of strength, and they stack:
current_passwordon the change-password and change-email handlers. Cheap, and it defeats a stolen token outright because the attacker still does not know the existing credential.- Auth-time freshness. OIDC carries
auth_timein the ID token. The server re-checks it on sensitive actions and rejects anything older than a few minutes, forcing a re-login. A refresh token alone cannot bumpauth_time, that is the whole point of the claim. - Step-up / re-authentication. Push the user through MFA or a fresh password prompt immediately before the action, scoped to that action.
If the email is changing, the old address gets a notification and ideally a reversal link, so a takeover is at least loud and recoverable.
How I test it, fast
- Log in, capture a sensitive-action request (password, email, MFA disable, recovery-phone change).
- Strip every field that is not the new value. Resend. If it still succeeds, the current-password gate is absent.
- Let the access token age, or use a token minted purely from a refresh token, so
auth_timeis stale. Resend. If it succeeds, there is no freshness check. - Confirm the old email or phone receives no notification. Silent change is the difference between a high and a critical.
Two minutes, four requests. The finding is not “no current-password field,” it is “a valid token, by any of the routes a token escapes, is sufficient for permanent account takeover, and nothing makes that loud.” Frame it as the primitive it is and the severity follows on its own.