Skip to content

gate-7: publicpage-unscoped-object-lookup false-positives an anonymous LOGIN-START endpoint (portaliq oidcStart) #471

Description

@rubenvdlinde

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions