Skip to content

fix(gate-7): a guard helper may spell its object noun after the auth token - #353

Merged
rubenvdlinde merged 1 commit into
mainfrom
fix/gate-7-verb-object-guard-helper-names
Aug 11, 2026
Merged

fix(gate-7): a guard helper may spell its object noun after the auth token#353
rubenvdlinde merged 1 commit into
mainfrom
fix/gate-7-verb-object-guard-helper-names

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

The defect

_GUARD_HELPER_NAME_RE's first alternative required the authorisation token (Admin/Access/Permission/Permitted/Owner/Allowed/Authori[sz]ed) to be the final CamelCase segment:

r"^(?:is|has|can|may)[A-Z][A-Za-z0-9_]*"
r"(?:Admin|Access|Permission|Permitted|Owner|Allowed|Authori[sz]ed)$"

That rejects the very common verb-object spelling where the object noun trails the auth token — canUserAccessAgent(), hasOwnerPermissionForRun(). These are genuine authorisation predicates, so every routed method delegating to one was reported as an unguarded IDOR.

The fix

The auth token may now be a complete CamelCase segment anywhere after the is|has|can|may prefix. A token is still required; only its position is relaxed.

r"(?:Admin|Access|Permission|Permitted|Owner|Allowed|Authori[sz]ed)"
r"(?:[A-Z][A-Za-z0-9_]*)?$"

Evidence

ConductionNL/hermiq @ development (cd23f547), full-scope run 31490144919, job 93776678440 — gate-7 FAIL, 3 methods, all three false positives:

Method Guard actually present
AgentsController.php:127 index filters every result through $this->canUserAccessAgent($agent, $userId) in-body
AgentVersionController.php:105 index loadAccessibleAgent()canUserAccessAgent(), returns null; caller returns Http::STATUS_NOT_FOUND
AgentVersionController.php:140 diff same

The 404-on-denial shape is the deliberate tenancy refusal this gate's own FAIL message endorses.

Gate-7 was proven NOT blind on that repo first. A textbook IDOR (#[NoAdminRequired] plantedIdor(string $id) doing a bare objectService->find(id: $id)) was planted into the tracked file lib/Controller/AgentVersionController.php — gate-7 enumerates via git ls-files -- lib/Controller, so an untracked plant is never scanned — and the count went 3 → 4. Plant removed.

Verification

Existing suite: 86 tests, all pass unchanged.

Added: 7 tests under VerbObjectGuardHelperNames — both hermiq shapes (in-body per-object filter; Pattern-4 transitive closure through a loader whose own name carries no token and whose body has no strict deny signal), plus abuse controls.

Revert check: with the pre-patch regex restored, the three PASS-shape tests go RED:

test_in_body_verb_object_predicate_clears_caller ......... FAIL
test_loader_delegating_to_verb_object_predicate_clears_caller ... FAIL
test_verb_object_name_without_an_auth_token_is_not_a_guard_name . FAIL

The first draft of the Shape-A fixture passed identically under the old regex — the method took no caller input, so the session-scoped/zero-reference exemption cleared it before the guard-helper pattern was ever consulted. The fixture now reads pagination params, so the object reference is real and the helper's name is the only thing standing between it and a finding. That near-miss is recorded in a comment on the fixture.

The negative controls are real boundaries, not structurally-always-flagged. With byte-identical bodies, only the helper name changed:

helper name verdict
canRender finding
canUserAccessWidget cleared
hasChanges finding
hasOwnerPermissionForDraft cleared

A positive control of the unguarded-fetch shape already exists twice (RealIdorViolationTest.test_no_guard_at_all_is_flagged, docblock form; ZeroInputReadOnlyEndpoints.test_tp_a_method_taking_an_id_is_still_reported, attribute form) and is deliberately not duplicated.

Fleet sanity sweep

Ran before and after over git ls-files 'lib/Controller/*.php' 'lib/Controller/**/*.php' in every local app with a lib/Controller:

app before after
hermiq 3 0
docudesk 2 2
hrmq 1 1
openregister, openconnector, opencatalogi, decidesk, doriath, larpingapp, openbuild, pipelinq, portaliq, procest, scholiq, shillinq, softwarecatalog, petstore, app-versions, nextcloud-app-template 0 0

Only the three hermiq false positives were cleared, fleet-wide. Each was opened and confirmed to carry a genuine per-object authorisation guard. The remaining findings (docudesk AnonymizationController::updateRelation, EmlPreviewController::preview; hrmq AdministrationController::setActive) are untouched.

Note: canUserModifyAgent — cited in passing while scoping this — correctly still does not match, since Modify is not an auth token.

…token

`_GUARD_HELPER_NAME_RE`'s first alternative required the auth token
(Admin/Access/Permission/Permitted/Owner/Allowed/Authorised) to be the
FINAL CamelCase segment, so `canAccess` matched but `canUserAccessAgent`
did not. That rejected the very common verb-object spelling, and every
routed method delegating to such a predicate was reported as an
unguarded IDOR.

MEASURED on ConductionNL/hermiq @ development (cd23f547), full-scope run
31490144919 / job 93776678440: gate-7 FAIL with 3 methods, all three
false positives —

  - AgentsController::index — filters every result through
    `canUserAccessAgent($agent, $userId)` in-body;
  - AgentVersionController::index and ::diff — both delegate to
    `loadAccessibleAgent()`, which calls `canUserAccessAgent()` and
    returns null, on which the caller returns Http::STATUS_NOT_FOUND —
    the 404-style tenancy refusal this gate's own FAIL message endorses.

Gate-7 was proven NOT blind on that repo before concluding this: a
textbook IDOR planted into the TRACKED file AgentVersionController.php
(gate-7 enumerates via `git ls-files -- lib/Controller`, so an untracked
plant is never scanned) took the count 3 -> 4. Plant removed.

An auth token is still REQUIRED; only its POSITION is relaxed.
`canRender` / `hasChanges` still do not match — they carry no auth token
in any position.

Tests: 86 existing pass unchanged; 7 added under
VerbObjectGuardHelperNames covering both hermiq shapes (in-body filter,
and the Pattern-4 transitive closure through a loader), plus four abuse
controls. The three PASS-shape tests were confirmed to go RED against
the pre-patch regex. The canRender/hasChanges controls were confirmed to
be real boundaries rather than structurally-always-flagged: renaming the
helper to canUserAccessWidget / hasOwnerPermissionForDraft, with a
byte-identical body, flips each to cleared.

Fleet sanity sweep, before/after over `git ls-files lib/Controller`:
hermiq 3 -> 0 (the three above, each verified to carry a genuine
per-object guard), docudesk 2 -> 2, hrmq 1 -> 1, and 16 other apps
0 -> 0. Nothing else in the fleet changed verdict; no unguarded method
was cleared anywhere.
@rubenvdlinde
rubenvdlinde merged commit 3c8da4c into main Aug 11, 2026
30 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/gate-7-verb-object-guard-helper-names branch August 11, 2026 12:56
rubenvdlinde added a commit that referenced this pull request Aug 11, 2026
Closes #360. #353 relaxed WHERE the auth token may sit in a guard-helper name but left the segment before it mandatory, so hasPermission() and canAccess() were still reported as unguarded IDOR. Making that segment repeatable and optional admits the token in any position; the token SET is unchanged and a token is still required. Measured strict superset: 400,000 fuzzed identifiers produced zero names the old regex matched and the new one does not, so this can only turn findings green and cannot redden any repo. Observed red-then-green: the gate-7 acceptance arm went 2 findings -> 0 and still goes red (2) under the reconstructed pre-fix regex; test_check_no_admin_idor.py 93 -> 95 tests, the new one failing on the old regex.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant