Harden OAuth redirect_uri validation (Posture D) - #1
christophdb wants to merge 1 commit into
Conversation
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>
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
|
Superseded by #2, released as v1.6.0. This PR identified the same 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 |
Summary
Hardens the managed-mode OAuth authorize flow against phishing / token theft via an unvalidated
redirect_uri. Only affectsmanagedmode (SeaTable Cloud); theselfhosteddefault has no OAuth endpoints and is unchanged.Problem
/authorizecollects the user's SeaTable API token and forwarded the resulting auth code (which unlocks that token) to an arbitrary, unvalidatedredirect_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/tokenredirect 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")localhost/127.0.0.1/::1)Plus:
/authorize.plainrejected; metadata advertises S256 only).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