A guard that only checked the first door
Our menu reader refuses to fetch private addresses. It checked the address you pasted and nothing after it — so a public link that redirected somewhere else walked straight past the check.
You can hand us a link to your menu and we will read the dishes off it. That means our server fetches an address a person typed, which is a thing to be careful with: a server on the inside of a network can reach places a browser cannot.
So there is a check. It refuses anything that is not http or https, anything pointing at localhost, anything in the private ranges, anything without a dot in it. That check is well tested and it is right.
It ran once, on the address you pasted. Then the fetch was told to follow redirects, and the redirects went wherever they liked.
The address you type is not always the address that answers
A link can answer with "not here, go there instead", and the thing on the other end decides where "there" is. A perfectly ordinary public address could send us somewhere the check would have refused on sight — and the words on whatever answered would have come back labelled as your menu.
A guard applied to the first hop and not the rest is not a guard. It is a guard-shaped thing that stops the careless and waves the deliberate through.
Following by hand
Redirects are now taken one at a time. Each destination goes through the same check as the pasted address, resolved against the hop it came from, because a redirect is allowed to be relative. Five hops is the limit — more than any menu site needs, fewer than a loop.
Would the platform we run on have refused those fetches anyway? Possibly. But "the platform probably blocks it" is not a guard we control, and it is not a sentence worth betting a customer's network on.
The smaller thing in the same fix
When a menu was read from a link, we recorded the address you pasted as where the menu came from. After a redirect that is simply the wrong answer — it is not where the menu lives. It now records the address that actually answered, which is the one you would need if you ever wanted to go back and look.
Why this took a year to notice
Because nothing ever ran that function. It had checks on its source — assertions that it did not contain certain things — and none that made it fetch anything. The bug was two lines below what those could see. That is a subject in its own right, and it has its own post.