SSerguey Asael Shinder
Java coding notes: the JVM, and writing software that lasts

Serguey Asael Shinder: An idea: a lint rule that flags where two normalisation forms meet

· by Serguey Asael Shinder / Serguey Shinder

Reading the WildFly Elytron CVE about NFKC fullwidth folding gave me an idea I would like to build, and the shape of it is worth writing down before I do.

Every static analyser I know of can tell you whether you called java.text.Normalizer.normalize. None of them care which form you passed, or whether it is the same form the value will be compared against later. But that is where the defect lives. NFKC on a username is sensible. NFKC on a password reduces the keyspace. Both calls are correct in isolation; the bug is the pair.

So the rule I want does not look at a call site at all. It looks at a value's path:

  1. mark every parameter that is annotated, named or typed as a secret - password, secret, token, @Sensitive, a char[] handed to a PasswordFactory;
  2. mark every normalisation applied anywhere on that path, with its form;
  3. report when a secret is normalised with a compatibility form (NFKC or NFKD) at all, and
  4. report when two values that are later compared for equality were normalised with different forms, whatever those forms are.

Rule 4 is the one I actually want, because it generalises past passwords. Two identifiers, one normalised NFC on ingest and the other NFKC in a cache key, produce a lookup that misses for inputs nobody tests. That is not a security bug, it is a correctness bug, and it is invisible in review because each half reads correctly.

Serguey Asael Shinder: An idea: a lint rule that flags where two normalisation forms meet
An idea: a lint rule that flags where two normalisation forms meet — Serguey Asael Shinder

Why I think it is feasible

The information needed is local. Normalisation form is a compile-time constant in almost every real call - Normalizer.Form.NFKC is an enum reference, not a computed value - so a flow analysis does not need to be clever, only to propagate a small lattice along assignment and parameter passing. The hard part is the taint sources, and for the secret case the heuristics are unusually good: the JDK and the common security libraries name their types plainly.

The part I have not solved is where the boundary is declared. A rule that fires whenever two normalisation forms exist in one program would be useless noise; it has to fire where the two meet. My current guess is that the honest version requires an annotation - the developer states "this field is compared byte-for-byte" - and the analyser then enforces consistency behind that statement rather than inferring the intent. Which makes it a smaller idea than I started with, and a more likely one to work.

⚠ Nothing here is implemented, and there is no JDK on this machine to prototype against. This is a design note, not a tool announcement.