gate-7 no-admin-idor reports publicpage-unscoped-object-lookup on portaliq/lib/Controller/SessionController.php::oidcStart. It is a false positive, and it keeps portaliq's development red — the Hydra Gates job fails on this one finding and nothing else, so the whole fleet-quality signal for that repo is stuck.
Why it is a false positive
oidcStart is the start of an OIDC broker login. There is no session yet, by construction — that is what the endpoint exists to create. The lookup it performs is already constrained on every axis:
public function oidcStart(string $org = "", string $provider = ""): Response {
$config = $this->orgConfig->resolveOidcConfig(orgSlug: $org, provider: $provider);
if ($config === null) {
return $this->oidcGenericError();
}
...
and resolveOidcConfig() fails closed to null on all of:
$provider not in the OIDC_PROVIDERS class-constant allow-list;
$org not resolving to an existing organisation;
- that organisation having no
oidc[$provider] block;
- a required field (
issuer/clientId/clientSecret) missing.
Every failure returns the same generic error, deliberately, so it cannot become an existence oracle.
There is also nothing to disclose. PortalPageController::index() passes orgResolver->resolve() — which includes oidcProviders (commented in-source as "SECRET-FREE — provider + label only") — as runtime config into a public template. Which organisation offers which login provider is published on purpose: a citizen has to see the "log in with DigiD" button. The only other value that reaches the caller is clientId, which is public in OIDC by specification.
Why I have not "fixed" it in the app
The two shapes the checker already clears are close but not matching:
_public_unscoped_selectors → allow-list validation wants a subscript into a class constant (self::MAP[$x] ?? null), as in decidesk OriController. Here the allow-list is an in_array() one hop down, inside the collaborator.
_public_refuses_on_scope_predicate wants the refusing if to have a predicate call in its condition. Here the call is on the preceding line and the if tests === null.
Restructuring correct code so a regex matches would be apologising to the gate rather than measuring it, and gate-7 has no reason-bearing opt-out (only security-change-has-tests, csrf-cochange and controller-exception-translation honour [hydra-gate-<name> exclude]). So I have left both sides alone and raised it here.
Suggested shape
A fourth clear for the publicpage- rule: a caller selector passed to a collaborator that fails closed to null, where the method refuses uniformly on null. That is the login-start / discovery shape generally — it will recur for every OIDC, SAML or eIDAS entry point in the fleet, none of which can have a session identity to scope by.
Whatever the fix, please keep it narrow. gate-7's dangerous failure mode is the false negative: it once read a 401 preamble as an authorisation guard and reported 0 findings across 18 apps, which became 167 real IDORs once patched. A clear that is too broad reintroduces exactly that.
Reproduce
python3 hydra-gates/scripts/lib/check_no_admin_idor.py $(find lib -name "*Controller.php")
# lib/Controller/SessionController.php:236 method=oidcStart rule=publicpage-unscoped-object-lookup
Note CI runs the gates full-repo, so _filter_preexisting is a no-op and this base-branch finding reddens every PR against development, not just the one that introduced it.
gate-7
no-admin-idorreportspublicpage-unscoped-object-lookuponportaliq/lib/Controller/SessionController.php::oidcStart. It is a false positive, and it keeps portaliq'sdevelopmentred — the Hydra Gates job fails on this one finding and nothing else, so the whole fleet-quality signal for that repo is stuck.Why it is a false positive
oidcStartis the start of an OIDC broker login. There is no session yet, by construction — that is what the endpoint exists to create. The lookup it performs is already constrained on every axis:and
resolveOidcConfig()fails closed tonullon all of:$providernot in theOIDC_PROVIDERSclass-constant allow-list;$orgnot resolving to an existing organisation;oidc[$provider]block;issuer/clientId/clientSecret) missing.Every failure returns the same generic error, deliberately, so it cannot become an existence oracle.
There is also nothing to disclose.
PortalPageController::index()passesorgResolver->resolve()— which includesoidcProviders(commented in-source as "SECRET-FREE — provider + label only") — as runtime config into a public template. Which organisation offers which login provider is published on purpose: a citizen has to see the "log in with DigiD" button. The only other value that reaches the caller isclientId, which is public in OIDC by specification.Why I have not "fixed" it in the app
The two shapes the checker already clears are close but not matching:
_public_unscoped_selectors→ allow-list validation wants a subscript into a class constant (self::MAP[$x] ?? null), as in decideskOriController. Here the allow-list is anin_array()one hop down, inside the collaborator._public_refuses_on_scope_predicatewants the refusingifto have a predicate call in its condition. Here the call is on the preceding line and theiftests=== null.Restructuring correct code so a regex matches would be apologising to the gate rather than measuring it, and gate-7 has no reason-bearing opt-out (only
security-change-has-tests,csrf-cochangeandcontroller-exception-translationhonour[hydra-gate-<name> exclude]). So I have left both sides alone and raised it here.Suggested shape
A fourth clear for the
publicpage-rule: a caller selector passed to a collaborator that fails closed tonull, where the method refuses uniformly onnull. That is the login-start / discovery shape generally — it will recur for every OIDC, SAML or eIDAS entry point in the fleet, none of which can have a session identity to scope by.Whatever the fix, please keep it narrow. gate-7's dangerous failure mode is the false negative: it once read a
401preamble as an authorisation guard and reported 0 findings across 18 apps, which became 167 real IDORs once patched. A clear that is too broad reintroduces exactly that.Reproduce
Note CI runs the gates full-repo, so
_filter_preexistingis a no-op and this base-branch finding reddens every PR againstdevelopment, not just the one that introduced it.