Skip to content

Refuse an OAuth parameter that arrives as a list where one value is meant - #3236

Merged
jeremy merged 1 commit into
oauth-token-digestsfrom
oauth-scalar-params
Oct 24, 2026
Merged

jeremy merged 1 commit into
oauth-token-digestsfrom
oauth-scalar-params

Conversation

@jeremy

@jeremy jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member

Held for Jeremy. Draft, stacked on #3192 (oauth-token-digests). It moves no converged train head.

The bug

Codex found this on #3228 (CIMD), and the CIMD lane fixed its own path there. The same class sits on the train itself. A name[]= form field, or a JSON array or object, reaches params as an Array or a nested Parameters. Handed to Oauth::Client.find_by(client_id: params[:client_id]) (authorizations_controller.rb#L74 at 617edb87b), an Array becomes an SQL IN list. So client_id[]=<id> found the client it named, getting past the exact-string comparison that every later check assumes.

The fix

Oauth::BaseController names the parameters that carry one value (RFC 6749 §3.1). Each endpoint refuses any of them that arrives as anything but a string or a JSON null:

  • When: after the endpoint's availability gate, so a dark server still answers 404, and before any lookup.
  • Error: invalid_request, or invalid_client_metadata at registration.
  • A JSON null passes through: each endpoint already treats it as absent. For a refresh, a null scope still means "no scope" and invalid_grant follows.

wrap_parameters false for OAuth controllers. Rails copied every JSON body under the controller's name, and for the token endpoint that's token, which is itself an OAuth parameter.

Endpoint Refused when not a string Answer
Authorize, GET and the consent POST client_id, redirect_uri Before the client lookup, on Fizzy's own error page; never redirected, since the target is the thing that can't be validated
response_type, code_challenge, code_challenge_method, scope, state (and resource, account_id once #3213 is below) Once the redirect target is validated: an error redirect to a vetted client, the local page otherwise, like every other authorize error
Token grant_type, code, code_verifier, redirect_uri, refresh_token, client_id, scope (and the exchange's subject_token, subject_token_type, requested_token_type, audience, resource once #3214 is below) invalid_request
Revocation token, token_type_hint, client_id invalid_request
Registration any of the names above; scope stays a permitted list (validated_scopes) invalid_client_metadata

Not changed:

  • client_secret stays with Oauth::ClientAuthentication. A non-string secret already fails there as invalid_client, which oauth_flow_test.rb pins.
  • The rest of registration's metadata (redirect_uris and grant_types lists, token_endpoint_auth_method, client_name) is validated by ClientsController's own checks, as before.

What it doesn't cover

  • Parameters read outside app/controllers/oauth: My::AccessTokensController (PATs), Connected Apps' revoke (a route id), API bearer authentication (the Authorization header), sessions.
  • The Basic header: parsed separately in ClientAuthentication, and only ever a string.
  • The denial's error: only compared to "access_denied".
  • A repeated bare resource=a&resource=b: Rack keeps the last value as a String, so this check can't see it. Audience grants for the hosted MCP server and bind each to one account (phase B) #3213's single_resource? raw count stays the guard for that.

Tests

test/integration/oauth_single_valued_parameters_test.rb has 11 tests. 7 fail at 617edb8; the other 4 pin behaviour that was already right (an array redirect_uri, dark-server 404, the token endpoint's string client_id, a refresh's null scope).

  • The IN-list case: client_id[] at authorize GET, at the consent POST, at the token endpoint and at revocation, each asserted with assert_no_queries_match(/oauth_clients/). Nothing touches the clients table before the refusal.
  • Arrays and objects in every other authorize parameter: an error redirect for a vetted client, the local page for a self-registered one.
  • Token, revocation and registration arrays, including registration still taking scope as a list.

The existing OAuth suites pass unchanged: 326 tests, 0 failures, OSS SQLite on the Mac. Full suites on thelio are below.

Expected conflicts

…eant

A name[]= form field or a JSON array or object reaches params as an Array or nested Parameters. Handed to find_by(client_id: ...), an Array becomes an SQL IN list: client_id[]=<id> found the client it named, past the exact-string comparison every other check assumes. Codex found this on #3228 (CIMD), which fixed its own path; this closes the class on the train's endpoints.

Each OAuth endpoint refuses any single-valued parameter (RFC 6749 §3.1) that isn't a string or null, after its availability gate and before any lookup: invalid_request at the token and revocation endpoints, invalid_client_metadata at registration, where scope stays a permitted list. The authorization endpoint refuses client_id and redirect_uri before it finds the client, on its own page, and the rest once the redirect target is validated, so they reach a vetted client as an error redirect like any other. client_secret stays with Oauth::ClientAuthentication, where a non-string secret already fails as invalid_client.

JSON parameter wrapping is off for OAuth controllers: it copied every JSON body under the controller's name, which for the token endpoint is token, itself an OAuth parameter.
@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-24T17:10:38.168529Z 227b388 Draft marked ready
🔒 Security Review ✅ Completed 2026-10-24T17:14:10.106627Z 227b388 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: 227b388212

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Status at 227b388. CI is green except Security, whose gem audit fails on the crass 1.0.7 advisory (GHSA-pq4g-8rxf-4g29), which was published today. This PR changes no gems. The base 617edb87b passed Security on 2026-10-18, before the advisory existed, so the bump belongs to the train, not this lane. The SaaS SQLite system tests failed twice for reasons outside this diff and passed on the third run. First a BackLinkNavigationTest sign-in redirect, which is a flake and not OAuth code. Then an apt install step. Codex: no findings. Review threads: none. Full suites on thelio: OSS SQLite 2146/0F, OSS MySQL 2146/0F, SaaS MySQL 2314/0F, saas/test 168/0F. Held as a draft.

@jeremy
jeremy marked this pull request as ready for review October 24, 2026 17:10
Copilot AI balanced review requested due to automatic review settings October 24, 2026 17:10
@jeremy
jeremy merged commit 6fd18cd into oauth-token-digests Oct 24, 2026
15 of 18 checks passed
@jeremy
jeremy deleted the oauth-scalar-params branch October 24, 2026 17:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The security-sensitive stacked change remains held for Jeremy’s review and has an unresolved JSON-format error-rendering issue.

1 open issue
What changed in this PR

This PR hardens Fizzy’s OAuth endpoints against arrays and objects supplied where a single string is expected, preventing malformed values from reaching client lookups.

Changes:

  • Adds shared parameter validation while preserving availability gates and registration’s list-valued scope.
  • Separates authorization errors by whether the redirect target can be trusted and disables JSON parameter wrapping.
  • Adds integration coverage for malformed parameters and existing null-scope behavior.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
test/​integration/​oauth_single_valued_parameters_test.rb Covers malformed input rejection across OAuth endpoints.
app/​controllers/​oauth/​tokens_controller.rb Validates parameter shapes before grant processing.
app/​controllers/​oauth/​revocations_controller.rb Rejects malformed parameters before credential checks.
app/​controllers/​oauth/​clients_controller.rb Uses registration-specific errors and permits scope lists.
app/​controllers/​oauth/​base_controller.rb Defines shared validation and disables JSON wrapping.
app/​controllers/​oauth/​authorizations_controller.rb Adds staged validation and shared local error rendering.

Review effort: Balanced

def render_error(error, description)
@error = error
@error_description = description
render :error, status: :bad_request
jeremy added a commit that referenced this pull request Oct 24, 2026
* origin/oauth-token-digests:
  Refuse an OAuth parameter that arrives as a list where one value is meant
jeremy added a commit that referenced this pull request Oct 24, 2026
With #3236 below, resource[]= and account_id[]= are refused by the single-valued check before any audience or account is read. Rails' list syntax isn't RFC 8707's repeated parameter: a bare key repeated stays invalid_target, from single_resource?. The test that sent a list of resources now expects invalid_request, and new tests cover a list account_id at consent and a list resource at the token endpoint.
jeremy added a commit that referenced this pull request Oct 24, 2026
* restack-oauth-audience:
  Refuse resource and account_id sent as lists as invalid_request
  Refuse an OAuth parameter that arrives as a list where one value is meant
jeremy added a commit that referenced this pull request Oct 24, 2026
With #3236 below, the exchange runs require_single_valued_parameters right after its availability gates, so subject_token, subject_token_type, requested_token_type, audience, resource, scope and client_id sent as lists are invalid_request before any read. Its own scope-only scalar check goes. A bare repeated resource stays invalid_target, from single_resource?.
jeremy added a commit that referenced this pull request Oct 24, 2026
* origin/oauth-token-digests:
  Refuse an OAuth parameter that arrives as a list where one value is meant
jeremy added a commit that referenced this pull request Oct 24, 2026
#3236's require_single_valued_parameters now refuses client_id[]= at authorize before any lookup, so set_client drops its own string check. A metadata document client is still reached only through its own URL. The array test now asserts the shared refusal with the switch off and on, and that no client query runs.
jeremy added a commit that referenced this pull request Oct 24, 2026
* origin/oauth-token-digests:
  Refuse an OAuth parameter that arrives as a list where one value is meant
jeremy added a commit that referenced this pull request Oct 24, 2026
* origin/oauth-token-digests:
  Refuse an OAuth parameter that arrives as a list where one value is meant
@jeremy jeremy mentioned this pull request Oct 25, 2026
4 tasks
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.

2 participants