Skip to content

fix(security): require an OAuth state on the callback (GHSA-75j5-qp3x-mx37) - #164

Merged
nictuku merged 1 commit into
mainfrom
fix/ghsa-75j5-oauth-state
Sep 22, 2026
Merged

nictuku merged 1 commit into
mainfrom
fix/ghsa-75j5-oauth-state

Conversation

@nictuku

@nictuku nictuku commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Closes the still-open item of GHSA-75j5-qp3x-mx37, reported by @Gal3m and @mohammad228.

Status of the three reported weaknesses

# Weakness State
1 Reflected XSS in the callback error page Fixed in 1.0.121 (GHSA-6v2r-2m4r-768m)
2 GET /token disclosed the stored auth artifact Endpoint removed in 1.0.121
3 No OAuth state minted or validated This PR

Changes

  • A cryptographically random state is minted per authorization request (new_oauth_state()), carried in the URL built by get_auth_url(), and required by the callback. A code whose state is missing, mismatched, or replayed is rejected and never reaches token_container. Comparison is secrets.compare_digest; a failed check does not burn the state, a successful one does.
  • The server binds 127.0.0.1 explicitly instead of whatever localhost resolves to.
  • It shuts down as soon as it has handled a valid callback, instead of idling out the full 180-second window (deferred to a timer thread so the response still reaches the browser and shutdown() is not called from the serving thread).
  • 9 new tests in tests/test_callback_server_state.py; the two existing success-path tests in test_callback_server_xss.py now mint a state first.

The report's fourth suggestion — bind a random high port — is not actionable. Meta validates the redirect URI against the list registered on the app, so the flow only works on pre-registered ports; 8080-8089 is a requirement, not an oversight. The state check is what makes an unsolicited callback useless.

Finding: the local OAuth flow has been broken since 0.4.4

Worth a separate decision, so flagging rather than fixing here. AUTH_RESPONSE_TYPE = "token" (implicit flow), so Meta returns the credential in the URL fragment — which never reaches the server. Commit a3fa12d (2025-07-17, "simplify callback server", first released in 0.4.4) removed the JS that read location.hash and the token_container["token"] write that went with it, but left the response type alone. Since then:

  • the callback's ?code= branch cannot fire, because Meta never sends a code in this mode;
  • nothing writes token_container["token"], so login() polls a key that stays None and times out after 5 minutes;
  • a user running --login or the get_login_link tool authorizes in the browser and lands on the "Unexpected Response" page.

So the state check added here guards a branch that is currently unreachable. It is still correct — it is required the moment the flow is repaired, and it is what the advisory asks for — but the flow itself needs a decision: repair it (switch to response_type=code with an exchange step, or restore fragment extraction) or remove the local login path and lean on META_ACCESS_TOKEN / the hosted MCP, which is already what the authentication-required message recommends.

Full suite: 558 passed, 9 skipped.

…-mx37)

get_auth_url emitted no state and the callback validated none (RFC 6749 10.12), so any page that could reach the local callback server could hand it an authorization code of the attacker's choosing.

A random state is now minted per authorization request, carried in the auth URL and required on the callback: missing, mismatched or replayed state means the code is rejected and never stored. The server also binds 127.0.0.1 explicitly and shuts down once it has handled a valid callback rather than idling out the 180s window.

The report's fourth item (random high port) is not actionable: Meta validates redirect URIs against the list registered on the app, so the flow only works on pre-registered ports.

Bumps to 1.0.124 and documents the advisory in SECURITY.md.
@nictuku
nictuku merged commit 9365b94 into main Sep 22, 2026
4 checks passed
PM-Labs pushed a commit to pmlabs-org/mcp-meta-ads that referenced this pull request Oct 11, 2026
…-mx37) (pipeboard-co#164)

get_auth_url emitted no state and the callback validated none (RFC 6749 10.12), so any page that could reach the local callback server could hand it an authorization code of the attacker's choosing.

A random state is now minted per authorization request, carried in the auth URL and required on the callback: missing, mismatched or replayed state means the code is rejected and never stored. The server also binds 127.0.0.1 explicitly and shuts down once it has handled a valid callback rather than idling out the 180s window.

The report's fourth item (random high port) is not actionable: Meta validates redirect URIs against the list registered on the app, so the flow only works on pre-registered ports.

Bumps to 1.0.124 and documents the advisory in SECURITY.md.
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