One key for the cupboard, not for every shelf
If you can open one ingredient, can you edit anyone’s pack sizes? A web address with two numbers in it asks that question, and the answer has to be checked at both levels.
Think about how an ingredient is stored. The product is one record — tinned tomatoes. Hanging off it are the pack sizes you buy and count in: a 2.5kg tin, a case of six, the base unit. Hanging off those are the suppliers who sell each pack and what they charge.
Three levels, and the address of the deepest one has to name all three. Open a pack to edit it and the request says, in effect, this product, that pack.
Which raises a question nobody normally asks: does anything check that the pack really belongs to the product?
Why it is not obvious
The outer check is the one everybody builds. Is this product yours? That gets written early, because it is the one a customer would notice. The inner one is easy to skip, because when you use the screen the pack always does belong to the product — the page only offers you your own.
But the address is not the screen. If the update only says "change the pack with this number", then being able to open one product is a key to every pack belonging to every product in the system, including the pack sizes and unit costs of a business you have never heard of. The outer gate was never wrong. It was just answering a different question.
We went looking
Sixteen places in our own product have a web address with two of these identifiers in it. We checked all sixteen. All sixteen were tied correctly, and there were four different ways of doing it — which is itself worth knowing, because it means a reviewer looking for one pattern would have called the other three unprotected.
The commonest is to name the parent in the update itself: change this pack and only if it belongs to that product. Cheap, local, reliable.
The better one, in the places that use it, is to load the parent's own list first and then look for the child inside it. Our recipe modifiers do this, and because they are three levels deep it is strictly stronger: it checks the grandparent too. The refusal that comes back is a sentence a human can act on — that answer is not in this question — instead of a bare rejection.
Where this shows up for an operator
You will never see this bug. That is exactly why it is worth asking about. There is no screen on which it misbehaves, no error in a log, nothing a busy service would surface. Its whole signature is that somebody else's pack size or buying price changes and neither business can explain it.
If you are comparing systems and you want one question that separates careful ones from the rest, this is a good one: when a request names a record inside another record, do you verify the relationship, or only the outer one? The answer will tell you a great deal about everything else you cannot see.
What we did about it afterwards
Finding all sixteen correct was a good day, but a one-off audit has a short shelf life — the seventeenth gets added next month by somebody who has not read this. So the list is now written down, each entry naming which of the four methods protects it, and a new address with two identifiers in it fails the build until somebody has said which. The failure message names the four options and what each means, so the next person does not have to find this article to know what to do.