Findings · 2026-09-23 · By VSNARY | Emmanuel Orta · 0 views
Authorization bug: why our owner's scans came back locked
The scan endpoint knew who he was and used it to skip the rate limit, then gated the reply as if he were a stranger. Two gates asked the same question two different ways.
The scan endpoint recognised the owner and used that only to skip the rate limit. When it sent the result back, it checked for a licence key or a customer session and nothing else, so the owner's own scans came back locked: the top finding in full and the rest as stubs. The report page asked the question correctly, which hid the defect. Both now read one predicate, and an anonymous scan is still gated exactly as before.
On the evening of 23 September the owner of this site told us his access did not work on scans. His browser carried a valid owner credential. The toolbar that only appears for the owner was on every page. The report pages opened in full. And yet a scan he ran from the homepage came back the way a stranger's does: the most severe finding in full, every other finding reduced to a title and a lock.
This post is the account of that defect. It is small, it touched nothing a customer pays for, and it did not change any score. We are publishing it because the shape of it is common in any product that gates content, and because the way it hid itself is the useful part.
What he saw, and what the site knew #
The first check was whether the credential worked at all. It did: the endpoint that reports who a browser belongs to answered owner, and the internal record behind a report, which only the owner can open, opened. So the site knew who he was. The question was where that knowledge stopped being used.
A scan is two requests from the reader's point of view. The first runs the scan and returns the result, which the homepage draws while it waits. The second is the report page at /r/<id>, which a reader lands on afterwards or opens from a link. We tested each on its own, as the owner and as an anonymous client.
| Surface | Recognised the owner? | Used it for | Owner's view before | After |
|---|---|---|---|---|
| Owner check endpoint | yes | the toolbar | correct | unchanged |
Report page /r/<id> | yes | opening every finding | full | unchanged |
| Scan endpoint: rate limits | yes | skipping the per-address caps | unlimited | unchanged |
| Scan endpoint: the reply | no | nothing | top finding + stubs | full |
The line that caused it: two gates, two questions #
The scan handler works out, near its start, whether the request comes from the owner. It uses that answer four times, all of them to decide whether a limit applies: the daily cap, the per-minute burst, the anonymous API allowance and the per-domain cap. The owner skips all four, correctly.
At the very end, the handler builds the reply. The comment above that code says why the reply is gated: the homepage draws this response for a free reader, so gating the report page and not this one would show everything for a second and then hide it. The gate itself asked only two questions: is there a live licence on the request, and is there a signed-in customer session. It did not ask the third question the rest of the handler had already answered. The owner was treated as unlicensed, so the reply went through the same withholding a stranger's does.
The report page had been written differently. Its gate asks whether the reader holds a licence, a customer session, a control-panel session, or owner access, and opens the record in full for any of them. That is why the owner saw a locked result and then, a second later on the report page, an open one. Each page was consistent with itself. They disagreed with each other.
How it was proved, both ways #
A fix to an access gate has two failure modes, and a test that only checks one of them proves half of it. Unlocking the owner is the obvious half. The other half is that an anonymous reader must stay exactly as gated as before, because a gate that opens for everyone would also pass an owner-only test.
So the check ran the real module twice, the build that was live and the patched build, each with two requests: one carrying a valid owner credential and one carrying none. On the live build the owner's scan came back with four of five findings locked, which reproduced his report. On the patched build the owner's came back with none locked, and the anonymous request still came back with four of five locked. Only after both halves held did it deploy.
After deploying, the same two readings were taken on the live site. The owner's browser ran a scan and received all four findings of that record in full. A request from outside with no credential ran the same scan and received one in full and three locked. The report page was unchanged throughout.
Why nothing caught it sooner #
Every test that existed passed, because each one tested a single surface. The report page test checked the report page. The rate-limit test checked that the owner was not throttled, and he was not. Nothing compared what the same person received from two routes that describe the same record.
It also hid itself well. The report page is where most reading happens, and it was right. The locked reply was on screen for the few seconds the homepage draws it before moving on. A reader who noticed would reload the report and see everything, which reads as a glitch rather than a defect.
This is the same class as an earlier one in the same product. A customer with a branding licence could set a logo and see it confirmed in settings while every report rendered unbranded, because the settings page and the report asked whether the licence allowed branding in two different ways. That fix left a comment saying the two gates must ask the same question. This time the lesson was learned in one place and not applied in another.
The rule it left: one access decision, called everywhere #
An access decision should exist once and be called everywhere, not be restated at each gate. Where a route both skips limits for someone and gates what it returns, both decisions have to read the same answer. The scan reply now reads the same owner check the limits already used, and internal and staff access follow the same path for the same reason.
There is a second rule, about testing. A gate test must include a reader who should be refused. An unlock that passes only an owner test has not shown it is an unlock; it may be a gate that stopped working. Our standing method for findings, measuring the thing the label claims and not the label, applies to access checks as much as to a crawler's view of a page.
What this did not affect #
No score, grade or finding changed; the defect was in what the owner was shown, not in what was measured or stored. The stored record was always complete. No customer's access was affected, because licence holders and signed-in customers were always recognised by the reply gate. The only reader who was wrongly refused was the one person the site exists to serve first. What a licence opens is set out on the pricing page, and the machine-readable shape of a report, gated and ungated, is described in the API documentation.
Findings behind this #
A defect that disappears on reload is still a defect. We found this one because the person affected said so plainly, and because the check that settled it compared two routes rather than trusting either. If you run a product with gated content, request the same record as the same user through every route that returns it, and add one request that should be refused.
Every figure above came out of this scanner.
Point it at your own domain and see the same measurements, free.
The main product
Found this on your own site? We fix it for $749.
Scan free to see where you stand. The fix is one site, every finding implemented and re-measured, with a sealed before and after.
Questions this post answers
Why did the owner see locked findings on his own scans?
The scan endpoint recognised the owner but used that only to skip rate limits. The code that built the reply asked only whether the request carried a licence or a customer session, so the owner's reply was withheld like a stranger's.
Why did the report page show everything?
The report page's gate already included owner access in its list of readers who see the full record. The two gates were written separately and asked the question differently.
Did the defect change any score or stored record?
No. The stored record was always complete, and grades are computed before any gate is applied. Only what was shown in the scan reply was affected.
How was the fix tested?
The real module was run before and after the change with two requests each: one with an owner credential and one with none. The fix had to unlock the owner and leave the anonymous request gated exactly as before.
Were paying customers affected?
No. Licence holders and signed-in customers were always recognised by the reply gate. The only reader wrongly refused was the owner.
Related findings
Comments
Comments are read before they appear. Nothing is published automatically, and no account is needed.
Writing about this? Facts, live figures and marks — every number on that page is dated and traceable to a scan.