A pre-registered, blind verification pass on elghaied/payload-reserve, a Payload CMS plugin for bookings and reservations. Free and unsolicited. The suite is strong, but the shipped default had a real hole. Verdict: one HIGH, disclosed and fixed.
The suite is not the problem here. I ran payload-reserve's own 676 tests as shipped (all green), then injected thirteen semantic defects one at a time and measured how many its suite caught. It caught eleven — a suite that tests its core booking logic well: capacity, conflicts, buffers, the hold lifecycle, the status machine. If the audit had stopped at the kill count, the write-up would read like a good one.
| result | |
|---|---|
| baseline suite (as shipped) | 676 / 676 green |
| planted defects caught | 11 / 13 |
| confirmed defects in shipped code | 3 (1 HIGH, 2 low) |
| findings disclosed privately | 3, all fixed in v4.1.1; a second batch of 2, addressed in v4.1.3 |
| the maintainer's own audit | 4.1.2: eleven security items and ten fixes, none of them mine |
The finding wasn't a planted mutant. It was in the shipped code, in the one place a planted-defect matrix doesn't look: the default access configuration. In the plugin's standalone mode, it creates the customer login collection itself but left collection access at the framework's default — "any authenticated user." So through the ordinary REST API, any logged-in customer could read, change, and delete every other customer's booking, and list every customer's personal details.
Because this is an access-control issue with a working exploit, I reported it privately first and withheld the exploit. Two smaller correctness findings rode along: date-only "closed day" exceptions resolved to the previous calendar day in any timezone west of UTC (so a business in New York closed the wrong day), and the reservations collection accepted a fractional guest count that the booking endpoint correctly rejected.
The maintainer reproduced all three findings against the exact commit I reported and shipped v4.1.1: reservation access scoped to the owner, delete made staff-only, the ownership check now run on update as well as create, customer notes moved behind a staff-only field rule, date-only fields resolved on their own calendar day, and fractional guest counts rejected. He also added a permanent regression test running the takeover scenario with access checks on.
I did not take the fix on trust. I cloned v4.1.1, reconstructed the withheld takeover exploit as a test, and re-ran it on the same harness as the original baseline. Every step that had succeeded is now denied — the read, the update, the delete, the password change, and the login as the victim. As a control, the victim's real password still authenticates, so the denials are genuine access control and not a broken request. The reconstructed exploit now ships in the engagement repo as the regression proof, green against the fix.
The first report carried only findings I had reproduced. Two more sat in my notes as untested design concerns and I left them out rather than send hunches. That was too strict for one of them, and I said so when I sent the second batch on the fourth: the hold endpoint that lets a customer claim a slot before checkout took no login and no per-caller cap, so one anonymous script placed holds on 24 future slots in a run and could re-hold each as it lapsed, keeping a schedule unbookable for as long as it ran. No data, no money, self-healing the moment it stops, so medium. And a low one: if the lookup of a booking's service failed mid-check, the code fell back to zero buffer time and carried on, so a back-to-back booking the buffer should block could slip through on a bad database day.
Before that email arrived, Eslam had already shipped 4.1.2: his own full audit of the plugin, prompted by the first report. Eleven security items and ten correctness fixes, most of them reachable by a customer login or an anonymous caller on the default configuration, and none of them in anything I sent. A maintainer who reads one report and then goes looking for the rest is the outcome this work is for, and it is the part of the story most write-ups skip.
Five days later, 4.1.3 addressed the second batch. I re-checked it statically, twice, and the honest result has three parts. The hold endpoint is closed by configuration, not fixed outright: two new options cap a signed-in customer's active holds and can refuse anonymous callers, but anonymous holds stay open by default because holds exist so a customer can claim a slot before an account does. The README now says plainly that a host who keeps them open must rate-limit the endpoint at the proxy, since a Payload handler cannot see a client address it can trust. I accept that reasoning and I say it here so nobody reads "fixed" and skips the proxy rule. The buffer fallback is fixed on one side: the read of each neighbouring booking's service now fails closed, logged at error level, with a three-case test. The read of the incoming booking's own service, the lines my report cited, still falls back to the default silently. My first re-check called that fixed as claimed. The second pass caught it, and the correction went to Eslam on the tenth before this page did. Same class, narrower than before, low.
Before reading a single line of the implementation, I wrote the attack plan from the plugin's public README alone, hashed it, and anchored the hash to an external clock — here, a timestamped record on the engagement repo — so nothing in the findings could have been reverse-engineered after the fact. You can check the chain in about a minute:
shasum -a 256 00-preregistration/ATTACK-PLAN.mdf172b405…99fef.The second report is anchored the same way: its body as sent ships in the engagement repo with an OpenTimestamps proof in Bitcoin block 965678, and the follow-up document there carries the line-level re-check table. The reproduction for the hold endpoint is withheld while the default configuration still permits it.
Open the engagement repo → Read the fix (v4.1.1) The follow-up (v4.1.3)
Every engagement ships the pre-registration, the baseline, the planted-defect matrix, and the findings, disclosed to you first, privately, with the same receipts you see here.