Root cause
Authentication & access control introduced
Have Claude write an OAuth provider implementation.
How AI contributed: Direct introductionPublished May 1, 2025 in
cloudflare/workers-oauth-providerAll 2 findings in this repository
OAuthHelpers.parseAuthRequest extracted redirect_uri from the authorize query and returned it without checking that it was in the registered client's redirectUris. completeAuthorization then built the 302 Location from that attacker-controlled URI and attached the authorization code. The token endpoint did check redirect_uri against the allowlist, but that is too late: if an application auto-approved a previously authorized client_id, a victim could be sent to a malicious callback with a live code. CWE-601; GHSA-4pc9-x2fx-p7vj / CVE-2025-4143 (PUBLISHED, not withdrawn); affected @cloudflare/workers-oauth-provider < 0.0.5, patched in 0.0.5 via PR #26.
Root cause
Have Claude write an OAuth provider implementation.
Fix
Makes authorization parsing asynchronous, looks up the registered client, and rejects any redirect_uri outside that client's allowlist before issuing an authorization code.
Case facts
+ }+ }++ private async getClient(env: any, clientId: string): Promise<ClientInfo | null> {+ try {+ const clientKey = `client:${clientId}`;+ const clientData = await env.OAUTH_KV.get(clientKey, { type: 'json' });+ return clientData;+ } catch (error) {+ return null;+ }+ }++ private async updateClientsList(env: any, clientId: string): Promise<void> {+ try {+ const clientsListKey = 'clients_list';+ const clientsList = await env.OAUTH_KV.get(clientsListKey, { type: 'json' }) || [];++ if (!clientsList.includes(clientId)) {+ clientsList.push(clientId);+ await env.OAUTH_KV.put(clientsListKey, JSON.stringify(clientsList));+ }+ } catch (error) {+ // If this fails, it's not critical+ console.error('Failed to update clients list:', error);+ }+ }++ private createOAuthHelpers(env: any): OAuthHelpers {+ return {+ parseAuthRequest: (request: Request): AuthRequest => {+ const url = new URL(request.url);+ const responseType = url.searchParams.get('response_type') || '';+ const clientId = url.searchParams.get('client_id') || '';+ const redirectUri = url.searchParams.get('redirect_uri') || '';+ const scope = (url.searchParams.get('scope') || '').split(' ').filter(Boolean);+ const state = url.searchParams.get('state') || '';++ return {+ responseType,+ clientId,+ redirectUri,+ scope,+ state+ };+ },++ lookupClient: async (clientId: string): Promise<ClientInfo | null> => {+ return await this.getClient(env, clientId);+ },++ completeAuthorization: (options: CompleteAuthorizationOptions): { redirectTo: string } => {+ // Generate a random authorization code+ const code = generateRandomString(32);++ // Generate a unique grant ID+ const grantId = generateRandomString(16);++ // Store the grant+ const grant: Grant = {+ id: grantId,+ clientId: options.request.clientId,+ userId: options.userId,+ scope: options.scope,+ metadata: options.metadata,+ props: options.props,+ createdAt: Math.floor(Date.now() / 1000)+ };++ // Store the grant with long TTL (or no expiry)+ env.OAUTH_KV.put(`grant:${grantId}`, JSON.stringify(grant));++ // Also store in user's grants list+ this.updateUserGrantsList(env, options.userId, grantId);++ // Store the authorization code with short TTL (10 minutes)+ const codeExpiresIn = 600; // 10 minutes+ env.OAUTH_KV.put(`auth_code:${code}`, grantId, { expirationTtl: codeExpiresIn });++ // Build the redirect URL+ const redirectUrl = new URL(options.request.redirectUri);+ redirectUrl.searchParams.set('code', code);+ if (options.request.state) {+ redirectUrl.searchParams.set('state', options.request.state);+ }++ return { redirectTo: redirectUrl.toString() };+ },++ createClient: async (clientInfo: Partial<ClientInfo>): Promise<ClientInfo> => { } return false; }- + /** * Finds the appropriate API handler for a URL * @param url - The URL to find a handler for@@ -1837,13 +1839,13 @@ class OAuthProviderImpl { // Find the appropriate API handler for this URL const url = new URL(request.url); const apiHandler = this.findApiHandlerForUrl(url);- + if (!apiHandler) { // This shouldn't happen since we already checked with isApiRequest, // but handle it gracefully just in case return this.createErrorResponse('invalid_request', 'No handler found for API route', 404); }- + // Call the API handler based on its type if (apiHandler.type === HandlerType.EXPORTED_HANDLER) { // It's an object with a fetch method@@ -2192,6 +2194,20 @@ class OAuthHelpersImpl implements OAuthHelpers { throw new Error('The implicit grant flow is not enabled for this provider'); } + // Validate the client ID and redirect URI+ if (clientId) {+ const clientInfo = await this.lookupClient(clientId);++ // If client exists, validate the redirect URI against registered URIs+ if (clientInfo && redirectUri) {+ if (!clientInfo.redirectUris.includes(redirectUri)) {+ throw new Error(+ `Invalid redirect URI. The redirect URI provided does not match any registered URI for this client.`+ );+ }+ }+ }+ return { responseType, clientId,AI-assisted change 3070a38e1a53a062018664858708b5119cfee6a5ea8fd1af61b3701fbfc66eb8 · Fix c6a72bbb24bbf41969ec68929a8dc498bc122fd465b799ca5d2e6bbce3e4c9d1