by Serguey Shinder
The call came in about fifty minutes after the deploy. A customer had refreshed her account page and seen somebody else's name at the top of it, along with a reference number that was not hers. She was calm about it, which made it worse, and she asked the question I would have asked, which was how long this had been happening.
The redesigned account overview had gone out that morning. It was a new endpoint, written in the newer style we had been moving towards, and it was correct in every way I would have checked in review. It authenticated. It authorised. It loaded only that customer's data and rendered only that customer's data.
In front of the application there was a content delivery network, and that network cached responses by path unless told otherwise. Every other authenticated page in the system was told otherwise, by a filter that had been added long ago and that added a header instructing caches to store nothing. The new endpoint did not run through that filter, because the newer style did not use it. Nobody removed the protection. It simply was not inherited, and inheritance was the only mechanism we had.
So for fifty minutes the network held one customer's page and served it to whoever asked for that path next. The window on any single copy was sixty seconds. The reach was everybody.
What I got wrong was not the header. It was that a security property of the system existed only as a habit of one code path. Nothing asserted it. Nothing tested it. If you had asked me that morning whether our authenticated pages were safe from shared caches I would have said yes with real confidence, and my evidence would have been that I remembered somebody adding a filter in about 2019.
The header went on that endpoint within ten minutes. The change that mattered took a week. We now enumerate every registered route at test time, call each one as a logged-in user, and assert that the response forbids storage unless that route has explicitly declared itself public. A new endpoint written in any style, by anyone, fails the build until it has made a decision.
There is a general shape here that I have stopped arguing with. Any protection that arrives by convention will eventually be missed by somebody writing perfectly good code in a slightly different way, and they will never know they missed it, because nothing they can see has changed.
Conventions protect the people who know them. Tests protect everybody else, including the version of me who joins in two years and has never heard of the filter.
– Serguey Asael Shinder
Leave a Reply