Security · Multi-vendor · Food halls

The gate checked the venue the request named

A vendor could zero-rate another kitchen’s dish or rename its menu section, because the shared check only read venues a request named. Two routes named none.


In a food hall every kitchen is a tenant of the same system. The thing that keeps one kitchen out of another's menu, prices and tax is a check that runs once, before any route, on every venue a request names: in the address, in the body, in an id that resolves to a venue. Name a venue you do not own and the request stops there. One route file opens by saying so, and relies on it.

That sentence is true. It also covers only requests that name a venue. Two kinds did not, and the gate had nothing to check.

A tax rate on anybody's dish

The VAT screen saves a venue id and a map of dish ids to rates. The venue id was checked, so naming your own venue satisfied the gate. The dish ids were never compared with it. The only other scope on the write was the company, and on our live database no venue has a company yet, so that clause matched everything. We drove it: a vendor granted one venue set another venue's dish from twenty per cent to zero-rated, and got a success. Somebody else's tax return, changed from a screen that looked like your own.

The fix scopes the write to the venue, and refuses rather than skipping. A query that quietly matches no rows answers success and changes nothing, so the screen would redraw with the old rate and no explanation. A save that looks like it worked is the worse outcome.

A section with no venue in the request

The next hour asked what else the gate could not see. Menu sections are addressed by their own id, and a section id is not something the gate resolves. The same vendor renamed another venue's section, which moves every dish in it, by design, onto a live guest menu under a name somebody else chose. Then archived it, taking it off that menu entirely. Fifty-seven live sections across nine venues were reachable that way.

Both routes now load the section, resolve its venue, run the same check, and answer not found rather than forbidden, so the request learns nothing about whether the section exists. The file header now says what the shared gate does and does not cover. The tests for both fixes pin the refusal and the three things that must keep working: a venue changing its own dish, the platform owner changing anything, and an invalid rate still refused first. A fence that refused everything would pass an attack-only test while the screen was broken.


Try it on tonight’s service.

Nothing to install, no card. Not better by the weekend? Close the tab.