For years, security awareness training taught people to look for bad grammar, odd phrasing, and generic greetings. Those were decent signals, because writing a convincing email in someone else's language, about their job, took effort.
That effort is now close to free. An attacker can scrape a target's public footprint and get a fluent, personal message that mentions their real project and their real manager, in any language.
So if your defense depends on a human noticing that an email reads wrong, it's time to replace it. This post looks at what the evidence says, what attackers are actually doing, and which controls still work when the writing is perfect.
What the numbers actually say
The figure you'll see most often is that AI phishing is "4.5 times more effective." It comes up in coverage of Microsoft's 2025 Digital Defense Report, which gives a 54 percent click-through rate for AI-automated emails against 12 percent for others.
Those numbers match a peer-reviewed study by Heiding, Schneier and colleagues, and it's worth looking at how that experiment was set up. It had 101 participants across four groups:
| Group | Click-through rate |
|---|---|
| Generic phishing (control) | 12% |
| Written by human experts | 54% |
| Fully automated by AI | 54% |
| AI with a human in the loop | 56% |
The 4.5x compares targeted AI emails with generic ones. Against skilled human attackers, AI tied. It's also a small sample.
The tie is still the important result, because the AI costs almost nothing per target. The automated pipeline also built accurate target profiles from public information in 88 percent of cases. Spear phishing used to be reserved for high-value targets because research took time. That limit is going away.
Field data points the same way, with smaller effects. Hoxhunt, which sells security training and so has a stake in the answer, ran AI against its human red teams over two years. AI went from 31 percent less effective in 2023 to 24 percent more effective by March 2025. That relative gain was reported as a 2.78 percent success rate against 2.25 percent, so in absolute terms the gap is small.
The losses are real either way. The FBI's report for 2025 put business email compromise at about $3 billion, and it was the first year the FBI counted AI-enabled complaints separately: more than 22,000 of them, with over $893 million in losses.
The email is only the first step
Better writing is the visible change, but the rest of the attack chain has improved too.
Fake login pages are quick to build. Okta observed attackers using Vercel's v0, a generative UI tool, to create convincing sign-in pages from plain text prompts, and some hosted brand assets on the same trusted infrastructure.
Reverse proxies defeat most MFA. Adversary-in-the-middle kits sit between the victim and the real site. The victim sees the genuine login, enters a real one-time code, and the proxy keeps the session cookie. Tycoon 2FA, linked to more than 64,000 incidents, had about 330 domains seized in March 2026. Researchers then reported its activity returning, so takedowns buy time and little more.
Some attacks skip the fake page. In device code phishing, the victim is told to enter a code on the real Microsoft sign-in page. Doing so authorizes the attacker's device. Every URL the victim sees is legitimate, so "check the link" doesn't help.
Put those together and the usual advice has a problem. The email reads well, the link may be real, and the MFA prompt is real.
Why training can't carry this
A study from UC San Diego followed more than 19,500 healthcare employees across ten simulated campaigns over eight months. Embedded phishing training reduced failure rates by less than 2 percent on average. Annual compliance training showed no significant effect, and three quarters of users spent a minute or less with the embedded material.
The researchers' recommendation was to focus on technical countermeasures. People should still report suspicious messages, and that reporting is valuable. But a control that needs every employee to be right every time won't hold, especially when the bait is written for them personally.
Score what the attacker can't polish
An LLM can rewrite the body of an email as many times as it likes. It has a much harder time changing where the message came from, how old the domain is, or whether you've ever dealt with that sender.
So look at infrastructure and context instead of prose. Here's a minimal signal extractor using only the Python standard library:
import re from difflib import SequenceMatcher from email import message_from_string, policy from email.utils import parseaddr KNOWN_DOMAINS = {"contoso.example", "payroll-vendor.example"} EXECUTIVES = {"dana reyes", "sam okafor"} SENSITIVE = re.compile( r"bank details|wire transfer|invoice|gift card|password|mfa code", re.I ) def domain_of(value: str) -> str: return parseaddr(value or "")[1].rpartition("@")[2].lower() def risk_signals(raw_email: str) -> list[str]: msg = message_from_string(raw_email, policy=policy.default) name = parseaddr(msg.get("From", ""))[0].strip().lower() sender = domain_of(msg.get("From", "")) reply_to = domain_of(msg.get("Reply-To", "")) auth = msg.get("Authentication-Results", "").lower() body = msg.get_body(preferencelist=("plain", "html")) text = msg.get("Subject", "") + " " + (body.get_content() if body else "") known = sender in KNOWN_DOMAINS signals = [] if "dmarc=pass" not in auth: signals.append("dmarc_not_passing") if reply_to and reply_to != sender: signals.append("reply_to_differs") if not known and any( SequenceMatcher(None, sender, d).ratio() >= 0.85 for d in KNOWN_DOMAINS ): signals.append("lookalike_domain") if not known and name in EXECUTIVES: signals.append("executive_name_outside_domain") if not known and SENSITIVE.search(text): signals.append("sensitive_request_from_unknown_domain") return signals Run it on a well-written message from a lookalike domain:
sample = """\ From: Dana Reyes <dana@c0ntoso.example> Reply-To: dana.reyes@mailbox.example Authentication-Results: mx.contoso.example; dmarc=pass Subject: Updated bank details for the Q3 vendor run Content-Type: text/plain Hi Priya, following up on our Tuesday review, could you use the attached details for this week's payment? """ print(risk_signals(sample)) # ['reply_to_differs', 'lookalike_domain', # 'executive_name_outside_domain', # 'sensitive_request_from_unknown_domain'] The message is polite and specific, and it passes DMARC, because DMARC only proves the mail is authorized by the domain in the From header. An attacker who owns c0ntoso.example can pass it perfectly. Four other signals still fire, and none of them depends on the writing.
To use this for real, only trust an Authentication-Results header added by your own mail gateway, since senders can forge their own. You'd also want domain age and a per-recipient history of who normally writes to whom.
LLMs have a place on the defensive side too. In the same Heiding paper, Claude 3.5 Sonnet detected 97.25 percent of phishing emails with no false positives on a set of 381. That's a small test, so use a model as one more signal alongside the ones above.
Make the stolen credential useless
The strongest defense removes the human judgment call from authentication altogether.
One-time codes and push prompts are phishable because a person can be persuaded to relay them. WebAuthn and passkeys work differently: the browser records the origin in the data that gets signed, and the credential is scoped to the site's domain. A proxy on a lookalike domain can't produce a valid assertion for the real site, however convincing its page is.
Here's the part of server-side verification that gives you that property:
import base64 import hashlib import hmac import json EXPECTED_ORIGIN = "https://login.example.com" RP_ID = "example.com" def b64url_decode(value: str) -> bytes: return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) def verify_context(client_data_b64: str, auth_data: bytes, challenge: str) -> None: client_data = json.loads(b64url_decode(client_data_b64)) if client_data["type"] != "webauthn.get": raise ValueError("wrong ceremony type") if client_data["origin"] != EXPECTED_ORIGIN: raise ValueError("origin mismatch") if not hmac.compare_digest(client_data["challenge"], challenge): raise ValueError("challenge mismatch") if auth_data[:32] != hashlib.sha256(RP_ID.encode()).digest(): raise ValueError("RP ID mismatch") This is for illustration. It leaves out the signature check, among other things, so use a maintained WebAuthn library in production. The point is that the check is a string comparison a persuasive email can't influence.
Two things can undo the benefit. If an account can still fall back to SMS or a password, attackers will push the victim to that path, so remove weaker methods for your highest-risk accounts. And passkeys protect the login, so stolen session cookies remain a risk: keep sessions short for sensitive areas and ask for a fresh passkey before high-impact actions.
A checklist for developers and small teams
Identity
- Require passkeys or hardware keys for admins, developers, and finance staff first.
- If you use Microsoft Entra and nobody needs device code sign-in, block it with Conditional Access. Test in report-only mode and keep your break-glass accounts excluded.
- Restrict which OAuth apps users can approve on their own.
- Publish SPF, DKIM, and a DMARC policy that reaches
p=reject, including on domains you own but don't send from. - Register or monitor the obvious lookalikes of your domain.
Money and access
- Verify changes to bank details by calling a number you already had on file, never one from the message.
- Require a second approver for payments above a threshold.
- Treat urgency as a reason to slow down, and write that into the policy so staff have cover when they do.
The product you ship
- Keep your emails boring and consistent. If you send links from random tracking domains, you're teaching users that odd domains are normal.
- Alert users about new devices and new passkeys.
- Audit features that let users send mail from your domain, like invites and shared documents. Phishers love a trusted sender.
- Give users a way to see and revoke active sessions.
When someone clicks
- Make reporting one button, and thank people who report, including those who clicked. Blame means you hear about it later.
- Revoke sessions and tokens as well as resetting the password. Token-based attacks survive a password change.
The takeaway
"Look for typos" was always a weak control that happened to work for a while. AI has ended it, and that's a useful forcing function. The defenses that survive are the ones that never depended on the attacker writing badly: origin-bound credentials, out-of-band verification for money, sender history, and fast session revocation.
Assume someone at your company will eventually click. Then make sure that click doesn't get the attacker very far.
Has your team moved to phishing-resistant MFA yet? If you tried and got stuck, I'd like to hear what blocked you.