What
gate-5 decides whether a routed method has an auth attribute by grepping a window of source lines:
head_block=$(sed -n "${start},${def_line}p" "$path")
if ! echo "$head_block" | grep -qE '#\[(PublicPage|NoAdminRequired|NoCSRFRequired|AuthorizedAdminSetting)\b|@(PublicPage|NoAdminRequired|NoCSRFRequired)\b'; then
That window is raw text. It does not distinguish an attribute from the same characters appearing inside a docblock comment.
So this passes the gate:
/**
* ADMIN ONLY: `#[NoAdminRequired]` is deliberately NOT used here.
*/
public function analytics(string $productId): JSONResponse
— while the method has no attribute at all.
Why it matters
This is the expensive direction for a security gate: a pass leaves no log, so a method that mentions any of the four attribute names in prose is silently exempt. Anyone can turn the gate off for a method by writing about it. The gate's own header already records finding a false negative of a different shape (the 20-line lookback borrowing a neighbour's attribute) and calls that out as "the expensive direction, and invisible because a pass leaves no log" — this is the same class.
Where it shows
openconnector's ProductSubscriptionsController:
Two methods with identical auth posture, one flagged and one not, decided entirely by prose.
The awkward part
There is no good way for an author to positively declare "admin only" on a plain controller method: absence of #[NoAdminRequired] is the admin gate, and @NoAdminRequired in a docblock would make it non-admin. So documenting the posture is genuinely the only option available — and it happens to also satisfy the regex, which is why this has not been noticed.
Suggested direction
Strip comments before matching, and give the "deliberately admin-only" case an explicit, greppable marker that is not a real attribute name — e.g. an @auth admin-only <reason> tag in the same family as @spec exclude / @e2e exclude. Then:
- an attribute is an attribute
- a deliberate admin-only endpoint is declared, not inferred from a missing line
- prose stops being load-bearing
I have deliberately not changed the gate here: doing so would flag subscribe() and every other method across the fleet currently passing on prose, and that sweep wants to land with the new marker rather than before it.
What
gate-5 decides whether a routed method has an auth attribute by grepping a window of source lines:
That window is raw text. It does not distinguish an attribute from the same characters appearing inside a docblock comment.
So this passes the gate:
— while the method has no attribute at all.
Why it matters
This is the expensive direction for a security gate: a pass leaves no log, so a method that mentions any of the four attribute names in prose is silently exempt. Anyone can turn the gate off for a method by writing about it. The gate's own header already records finding a false negative of a different shape (the 20-line lookback borrowing a neighbour's attribute) and calls that out as "the expensive direction, and invisible because a pass leaves no log" — this is the same class.
Where it shows
openconnector's
ProductSubscriptionsController:subscribe()— no attribute; passes the gate solely because its docblock says`#[NoAdminRequired]` is deliberately NOT used hereanalytics()— no attribute, no such sentence; flagged (fix(gateway): analytics is admin-only, and a failed activation no longer 500s openconnector#1165)Two methods with identical auth posture, one flagged and one not, decided entirely by prose.
The awkward part
There is no good way for an author to positively declare "admin only" on a plain controller method: absence of
#[NoAdminRequired]is the admin gate, and@NoAdminRequiredin a docblock would make it non-admin. So documenting the posture is genuinely the only option available — and it happens to also satisfy the regex, which is why this has not been noticed.Suggested direction
Strip comments before matching, and give the "deliberately admin-only" case an explicit, greppable marker that is not a real attribute name — e.g. an
@auth admin-only <reason>tag in the same family as@spec exclude/@e2e exclude. Then:I have deliberately not changed the gate here: doing so would flag
subscribe()and every other method across the fleet currently passing on prose, and that sweep wants to land with the new marker rather than before it.