feat(qa): pass H, authorization and abuse

The passes stopped at G, and A-G share an assumption that hides an entire class
of defect: every one of them asks a legitimate user to do legitimate things.
Nothing looked at what happens when the caller is not who they claim, does not
own what they ask for, or asks too often.

That assumption has already cost this portfolio once. requireCoupleContext in
Closer-Couples never verified the caller belonged to the couple whose data was
returned -- authentication present, correct, and proving nothing about
ownership. No pass A-G would have found it.

Pass H covers six cases: authenticated endpoints called logged-out, a list
endpoint checked for rows the caller should not see, User A requesting User B's
object by id, a privileged route opened as an ordinary user, the expensive
endpoint hit repeatedly, and the built bundle and localStorage inspected.

The organising sentence, which is the reason it is a separate pass rather than
more rows in B: authenticated is not the same as owning, and neither is the
same as permitted.

closes #2

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
null 2026-08-17 22:53:53 -05:00
parent 8b2e4121dd
commit 29fb908932
2 changed files with 19 additions and 0 deletions

View File

@ -23,6 +23,7 @@ Review trigger: Any QA round run
| E — End of loop | **Blocked** | <what blocks it, and the issue number> | | E — End of loop | **Blocked** | <what blocks it, and the issue number> |
| F — Accessibility | Not run | <why, and when it will be> | | F — Accessibility | Not run | <why, and when it will be> |
| G — Performance | Pass | <the numbers, not "felt fine"> | | G — Performance | Pass | <the numbers, not "felt fine"> |
| H — Authorization and abuse | Not run | <which of the six cases were exercised, and as which user> |
## Standing gaps ## Standing gaps

View File

@ -33,10 +33,28 @@ skipped one is visible.
| E | The end of the loop — the state that is hardest to reach on purpose | | E | The end of the loop — the state that is hardest to reach on purpose |
| F | Accessibility: keyboard only, screen reader labels, contrast, text scaling | | F | Accessibility: keyboard only, screen reader labels, contrast, text scaling |
| G | Performance under the load this product will actually see | | G | Performance under the load this product will actually see |
| H | Authorization and abuse: every authenticated endpoint called logged-out; a list endpoint checked for rows the caller should not see; User A requesting User B's object by id; a privileged route opened as an ordinary user; the most expensive endpoint hit repeatedly; the built bundle and `localStorage` inspected for anything secret |
<Add, remove and rename to fit. A pass that never applies is noise; a pass that <Add, remove and rename to fit. A pass that never applies is noise; a pass that
is always skipped is a lie.> is always skipped is a lie.>
### Why H is separate from B
**Authenticated is not the same as owning, and neither is the same as
permitted.** Login is the front door; every room inside still needs its own
lock. Pass B walks the flow as a real user, which means it walks it as a user
who is entitled to everything it touches — so B cannot see the entire class of
defect where the entitlement was never checked.
This has already cost this portfolio once. `requireCoupleContext` never verified
that the caller belonged to the couple whose data was being returned:
authentication was present, correct, and proved nothing. No pass A-G would have
looked for it, because every one of them asks a legitimate user to do legitimate
things.
Run H as somebody who is not entitled: logged out, logged in as the wrong
person, and logged in as the right person asking too often.
## What counts as a finding ## What counts as a finding
A finding needs: what was done, what happened, what should have happened, and A finding needs: what was done, what happened, what should have happened, and