Home/Journal/Pattern
Patternauthenticationaccount-takeoveridentity

Email Is Not Identity

When a server trusts a caller-supplied email, domain, or identity claim as proof of who you are, anyone can become anyone. Here is the pattern and the fix.

Rahul Dharan··8 min read

What this is

Email is not identity. An email address, an email domain, a claimed user ID, or a “prove you own this by naming its URL” challenge is a value the caller supplies. It is a claim, not a credential. This pattern is what happens when a server treats one of those caller-supplied values as proof of who the caller is, instead of binding identity to something the server itself verified. The moment a request can say “I am this person” and be believed, anyone can be anyone.

The clearest form is a password reset or login OTP that gets sent to a destination the caller names in the request. The code leaves the building and lands in the attacker’s inbox. But the pattern wears several faces, and they all share one root: the server trusted the envelope instead of checking the signature inside it.

Why it keeps happening

Identity feels solved. Every framework ships a login form, every tutorial wires up a reset flow, and the happy path works on the first try. So the question “what actually proves this caller is who they say they are?” rarely gets asked out loud, because the code already runs.

The trap is that the happy path and the attack path look identical in a demo. When a developer tests their own reset flow, they type their own email, the code arrives, and everything works. Nobody in that moment types a different person’s account with their own inbox as the destination, because that is not how you test your own feature. The gap only appears when someone treats the email field as an input to attack rather than a detail to fill in.

The same pressure that makes teams ship fast, an AI-drafted auth handler that “looks right,” a social-login integration copied from a snippet, an internal tool that assumes everyone hitting it already works here, quietly moves the trust decision to the wrong place. The server ends up trusting the caller to tell the truth about themselves. On the open internet, that is not an authentication system. It is an honor system.

How the attack works

The mechanism is the same across every variant: find the request field that carries identity, and change it.

  1. Find the flow that acts on identity. A password reset, a login, a “resend my code,” a social-login callback, an internal pre-authorization check.
  2. Locate the caller-supplied identity value. The email in the reset body. The destination address. The {id, email} the client posts after a social handshake. The domain the server reads off an address. The server URL a tenant is asked to “prove” it controls.
  3. Substitute the target. Put the victim’s account in one field and, where the flow allows it, the attacker’s own destination in another. Or assert the identity the attacker wants and see if the server verifies it.
  4. Collect what the server hands back. A reset code delivered to the attacker. A session minted for the asserted email. Staff access granted on a claimed domain. Tenant control granted on a named URL.

There is no exotic tooling here. The exploit is usually a single well-formed request. That is what makes the class dangerous: it is not a race, not a brute force, not a chain of ten steps. It is one request that the server was never told to distrust.

What it looks like in the wild

We have found this across companies that share nothing but the shape of the mistake, which is what makes it a pattern.

The consistent theme: in every case, a value the caller controlled was allowed to answer the question “who is this?” The server never independently checked.

How to tell if you are exposed

A short self-check for any flow that grants access, sends a code, or returns someone’s data:

  1. Follow the identity value. For each such endpoint, ask: where does the server learn who the caller is? If the answer is “from a field in the request,” that field is a substitution target.
  2. Test the reset destination. Trigger a password reset for an account you do not own, and try to steer the code or link to a destination you control. If it arrives, the flow trusts the caller.
  3. Inspect the social-login callback. Confirm the server verifies a signed token from the provider and reads identity out of that verified token. If it accepts an email or user ID posted by the client, it is trusting the client.
  4. Check any domain-based access. If staff or tenant privileges hinge on an email domain, treat that as failing until proven otherwise. A domain in a string is not membership in an organization.

How to fix it

  1. Bind identity to a verified credential. The caller’s identity comes from a session or a token the server issued and validates on every request, never from a value the caller typed. If the request has to say who it is, it is already broken.
  2. Send codes and links only to a destination on file. A reset code or login OTP goes to the address or phone the server already stored for that account, chosen server-side. Never to a destination supplied in the request.
  3. Verify signed identity tokens; never trust client-asserted claims. For social login and any federated identity, validate the provider’s signature and read the identity out of the verified token. Discard any identity the client asserts on its own.
  4. Never treat an email domain as authorization. Employment, tenancy, and role are verified against a directory or an invite the server issued. A domain suffix proves the shape of an address and nothing else.
  5. Separate “who you claim to be” from “who you are.” Every access decision runs against the verified identity, not the claimed one. If those two can ever differ in your code, an attacker will make them differ.

The takeaway

An email address is something a caller types, not something a server verified. Any system that lets a request assert its own identity, and believes it, can be impersonated with a single request. Bind identity to a credential you issued and validate, and the whole class disappears. This is one of the most common ways we take over accounts, and it is one of the easiest to close.

This pattern shows up again and again in our engagements. Two clean instances are written up in full: The Password Reset That Emailed the Attacker and The Social Login That Trusted the Client.

Frequently asked

What does 'email is not identity' mean in application security?
It means an email address is a claim, not a credential. A caller can type any email into a request. If the server treats that value as proof of who the caller is - sending them a reset code, minting them a session, granting them access - then anyone can impersonate anyone. Identity has to be bound to something the server itself verified, not to a value the caller supplied.
How does a password reset become an account takeover?
If the reset endpoint sends the one-time code or reset link to a destination the caller supplies in the request, rather than to the address already on file for that account, the code goes to the attacker. One request, and the attacker holds a valid reset for any account, including admin.
Why is trusting a client-asserted social-login identity dangerous?
Social login is only secure when the server verifies a signed token from the identity provider and reads the identity out of that verified token. If the server instead accepts an unsigned id and email sent by the client, anyone can send any email and be issued a session for it. The signature is the whole security property; skipping it turns login into a form where you type who you want to be.
Is an email domain enough to prove someone is an employee?
No. An email domain is a string in a field. Treating 'the address ends in @company.com' as proof of employment lets anyone who types that domain inherit staff access. Employment has to be verified against a real directory or an invite the server issued, never inferred from a claimed address.

This is one finding from a harness that runs continuously. See how Greywatch finds, proves, and fixes them.

How it works →