Skip to content

Harden OAuth redirect_uri validation (Posture D) - #1

Closed
christophdb wants to merge 1 commit into
mainfrom
harden-oauth-redirect-uri
Closed

christophdb wants to merge 1 commit into
mainfrom
harden-oauth-redirect-uri

Conversation

@christophdb

Copy link
Copy Markdown
Member

Summary

Hardens the managed-mode OAuth authorize flow against phishing / token theft via an unvalidated redirect_uri. Only affects managed mode (SeaTable Cloud); the selfhosted default has no OAuth endpoints and is unchanged.

Problem

/authorize collects the user's SeaTable API token and forwarded the resulting auth code (which unlocks that token) to an arbitrary, unvalidated redirect_uri. An attacker could craft a link on the real Cloud domain (.../authorize?redirect_uri=https://evil.example/cb), trick a victim into pasting their token, and capture the code. PKCE, the /token redirect match, and open DCR don't prevent this because the attacker initiates the whole flow.

Fix — classify redirect_uri (Posture D, "curate instead of enumerate")

redirect_uri Behavior
Loopback (localhost/127.0.0.1/::1) allowed, no warning (code lands on the user's own machine)
Configured trusted https host allowed, no warning
Unknown https host allowed, but warning banner before token entry
Remote http / non-http(s) scheme / malformed rejected (400)

Plus:

  • Destination host is always shown on the authorize page.
  • Enforced on both GET and POST /authorize.
  • PKCE restricted to S256 (plain rejected; metadata advertises S256 only).
  • New env SEATABLE_OAUTH_TRUSTED_REDIRECT_HOSTS (comma-separated; empty = only loopback trusted).

Tests

Existing OAuth tests updated to valid URIs + 8 new cases (reject remote http / bad scheme / malformed, loopback without warning, unknown https with warning, POST rejection, plain-PKCE rejection, configured trusted host without warning). Full suite (210) green, typecheck + lint clean.

Known limitation

Mitigates the reported vector proportionally but doesn't remove the underlying "paste a long-lived token into a web form" model. The robust rebuild (delegating auth to cloud.seatable.io with a real session/consent) remains the long-term path and requires an OAuth authorization server in SeaTable core. See SECURITY_OAUTH_REDIRECT_HARDENING.md.

🤖 Generated with Claude Code

Validate redirect_uri in the managed-mode OAuth authorize flow to mitigate
phishing/token theft via unvalidated redirect targets:

- classifyRedirectUri(): loopback + configured trusted https hosts are allowed
  without a warning; unknown https hosts are allowed but the user is warned
  before submitting their token; remote http, non-http(s) schemes and malformed
  URIs are rejected.
- Enforced on both GET and POST /authorize; destination host is always shown.
- PKCE restricted to S256 (plain rejected; metadata advertises S256 only).
- New env SEATABLE_OAUTH_TRUSTED_REDIRECT_HOSTS (comma-separated hosts).
- Docs (.env.example, CLAUDE.md, SECURITY_OAUTH_REDIRECT_HARDENING.md) and tests.

Only affects managed mode; selfhosted default has no OAuth endpoints.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
christophdb added a commit that referenced this pull request Aug 25, 2026
Closes the ST-01/ST-02 findings from the external security report. Supersedes #1.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_014k5RGUNUDegp7Fhsotwiyi
@christophdb

Copy link
Copy Markdown
Member Author

Superseded by #2, released as v1.6.0.

This PR identified the same redirect_uri weakness seven weeks before the external report and its analysis was correct — in particular that PKCE, the token-endpoint redirect match and open dynamic registration do not prevent the attack when the attacker initiates the flow.

The narrower fix here was not merged. #2 rebuilt the OAuth bridge instead: client registrations and issued tokens are sealed envelopes, the raw SeaTable API token is never handed out, and every binding is enforced. The SEATABLE_OAUTH_TRUSTED_REDIRECT_HOSTS idea from this PR is carried over, with different semantics: an unknown remote https callback is not warned about but met with an un-skippable confirmation step, while loopback and private-use schemes pass without friction because the code never leaves the user's machine.

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