Serguey Asael Shinder: Escaping is a property of the destination, not of the string
There is no such thing as a safe string. The same seven characters are harmless in a log line, structural in JSON, executable in HTML and a syntax error in a CSV cell. Safety is not a property the value can carry; it is a relation between the value and the place it is about to land.
Say it that way and the design consequence follows immediately: every escaper, every quoter, every formatter is a function of two arguments. The value, and the destination. And the second argument is the one that gets lost, because it is usually not written down anywhere — it is implied by context, inherited from a parent, or read from ambient state.
Today's Quarkus advisory is the cleanest example I have seen this year. The escapers were called. They were handed a value whose content type had not been propagated into the sub-template, and an escaper that does not know whether it is writing HTML or JSON has exactly one correct behaviour: do nothing. No call was forgotten and no check was skipped. The destination argument arrived empty, and the function returned its identity.
Why this failure mode is worse than a forgotten call
A forgotten escape is visible. There is a place where the call should be and is not, so a reviewer can find it, a linter can flag it, and a test that renders one hostile string catches it.
A dropped destination is invisible in all three of those ways. The call is there, in the reviewed line. The linter sees an escaper being used. And the test passes, as long as the test exercises the path where the context happens to survive — which is the ordinary path, because the context survives on the ordinary path. What fails is the nesting: the sub-template, the included fragment, the recursive render, the helper that builds a value in one context and returns it into another.
So the bug lives exactly where the reviewer's attention does not: not in a line, but in a boundary between two lines that both look right.

The same shape, with nothing to do with security
"TITLE".toLowerCase() has the identical structure. Lower-casing is not a property of a string either — it is a relation between the string and a language, and the no-argument overload fills the missing argument from ambient state. When that state is Turkish, a protocol key stops matching a constant in your source. String.format("%.2f", x) fills the same gap from a different ambient value, and can return digits that are not ASCII.
Nobody calls those security bugs, and they are the same defect: a two-argument function invoked with one argument, where the second is supplied by the environment rather than by the caller.
What to do about it, in order of how much it buys
Make the destination a parameter, not an ambient value. If a function's output correctness depends on where the output goes, the destination belongs in its signature. Every API in this family already offers that overload — toLowerCase(Locale), format(Locale, …), an escaper that takes a content type. The overload without it is shorthand for a decision you did not make.
When a value crosses a boundary, carry the context with it or refuse to cross. A sub-render, a nested template, a helper that returns markup, a message passed to another layer: each is a place where the destination can be dropped. Pick one and write down what happens there. "Inherits from the parent" is an answer; "the parent passes it in" is a better one; nothing written down at all is the Quarkus bug.
Escape at the point of output, never at the point of input. Input-time escaping has to guess which of several destinations the value will eventually reach, and it guesses once for a value that may be rendered three ways. Output-time escaping knows the answer, because it is standing at the destination. This is old advice and this is the reason for it.
Then treat "no context available" as an error, not as a default. The reason the advisory's bug was silent is that a missing variant meant skip escaping rather than fail. A function that cannot determine its destination has not been given enough to do its job, and saying so loudly is the only version of this that shows up in a test run instead of in a CVE.