> They put in an "attempt to evaluate strings" feature.

You have to evaluate to strings eventually though. A shell console/command prompt has no concept of SQL-style bind-variables. If you are logging "username: {}" that eventually has to be evaluated to a string sometime.

> It was a complete misfeature from start to finish.

No, there was a legitimate demand for this at the start. If you want to log instance-specific information (like JVM container name) the facility provided for that in Java is JNDI. And obviously it would be nice to have "what's the instance that produced this particular message" in the error logs, that's self-evidently helpful and desirable.

The problem is that JNDI is a foot-howitzer, it can also be used to load and execute arbitrary code from remote locations among many other things. Similar to how binary deserialization from remote sources basically cannot be used safely in Java (because a deserialized object can be in an "illegal" logical state and bypasses constructors and other assertions, an object type cannot be known before it is deserialized (so it can be of any class-type), and deserializers can run arbitrary attacker-controlled code before it hits any validation layer) this is not immediately apparent just from the fact that you are using JNDI, but, it's safe because we're not going to call attacker-controlled remote code through JNDI, right? Why would we attack ourselves?

And then the final piece is that (as you note) they fucked up the order of evaluation. JNDI is evaluated at the very end of the stringification process, meaning an attacker can include their own JNDI calls in the string and then it will be evaluated like developer-controlled JNDI. That is the fundamental problem here - this is bog-standard SQL-injection-attack style stuff, you should never be able to escape an injected/bound variable, but JNDI evaluation takes place after stringification.

All that really needed to be fixed was the last part. But honestly the dogma of the Java world has moved away from this whole approach in the first place - people were doing multi-container-per-JVM back in 2000s when memory was scarce, so you wanted to know that instance information so you could tell what instance was generating a message. But nowadays people do 1-container-per-JVM (and many JVMs scaled in a cluster) and you can handle that with runtime variables/etc that let the instance know where it is from external oracles. So there is no need to touch that in the first place.

It's all very much functionality that was demanded by real-world users... in the early 2000s. But Java refuses to clear some of the cruft from their APIs because it would produce (gasp) breaking changes in legacy applications... sort of the exact opposite of the Python 3 mindset. And JNDI is one of those interfaces that was just way too powerful when it was created. As mentioned, binary deserialization is another... you can't really use that in almost any scenario because the attack surface is just too large.

https://cheatsheetseries.owasp.org/cheatsheets/Deserializati...

(JNDI plus deserialization is a good one too, up until a couple years ago you could specify the class/deserializer as living on a attacker-controlled remote source, because JNDI can do anything!)

https://www.veracode.com/blog/research/exploiting-jndi-injec...