This post has two halves: what "escape your output" actually means once the obvious answer stops working, and then a study of whether current AI assistants get that harder version right.
It continues a series on what coding assistants actually produce when a non-expert asks them for WordPress code.
Start with a line that passes review and still leaves a hole: There is an escaping function wrapped around the value.
A for finds it.
A quick review passes it.
Now set to .
The link still runs when the visitor clicks it. escapes characters that matter in HTML text, like and .
It does nothing about a dangerous URL scheme.
The code is escaped.
It is escaped for the wrong context.
From the outside, correct escaping and wrong-context escaping look identical.
A URL is a different context and needs its own escaper.
That escaper is , and it returns an empty string for a URL.
Which escaper goes where Escaping is context-dependent. "Sanitize on input, escape on output" is the rule the first post of this series covers, but "escape on output" hides a second decision: which escaper.
The answer depends on where the value lands on the page.
Where the value lands Escaper Visible text between tags () Inside an attribute () A URL slot (, ) , which also enforces a safe scheme Inside a tag attribute that holds JavaScript () , scoped to this slot by core's own documentation Any value handed to JavaScript inside a block () , which writes its own quotes; add when the value is untrusted HTML you want to keep, like a formatted post body with an explicit allow-list Six functions, one job each.
The mistake is almost never "forgot to escape".
It is "escaped for the wrong context", and the on a URL above is the cleanest example.
The functions and their contexts are WordPress's own; what this table adds is only the habit of choosing by where the value lands.
One row deserves a warning, because WordPress's own guidance reads differently.
That page shows inside a block.
Core's own documentation for the function scopes it to a tag attribute, with as its example.
The two disagree, and the second half of this post is partly about why that matters.
The simple contexts are not where this series has found slips.
An earlier experiment tested the URL case: given a plain "show a link" task, 8 runs out of 8 reached for unprompted.
So the study had to go somewhere genuinely subtle.
That place is JavaScript.
Why JavaScript is the hard case Dropping a value into an inline script is an ordinary thing to do: If contains , the browser does not care that you meant it as text inside a JavaScript string.
The HTML parser scans the raw contents of the element for its closing tag, finds one, closes it early, and parses whatever follows as new HTML.
That is a breakout: the value escapes its slot and becomes markup.
The parser is a little more general than the plain form: it looks for matched without regard to case, followed by whitespace, a or a , so closes the tag too.
Neutralizing the exact lowercase string is not enough.
This is how the HTML specification defines script parsing, and one detail from it drives everything below: inside a script, character references are not decoded. stays the literal text .
It never turns back into .
That fact splits the safe approaches from the unsafe ones.
Every row of the truth table below was verified against WordPress core, the PHP manual, and a probe script that runs the payload through the real functions and prints the bytes.
What the code does What the browser receives Breaks out?
Raw concatenation, no escaping Yes No (default) No Yes No (In PHP the flags combine with the bitwise | operator.
The is for table readability; the value is identical for these flags.) The two safe middle rows are the point of the study, and they are safe for different reasons. blocks the breakout and corrupts the value.
It entity-escapes and into and , which are inert inside a script.
But by the same rule those entities are never decoded there, so the JavaScript strin