Skip to content

fix(security): harden OAuth, auth, and API route security#29

Closed
jdguggs10 wants to merge 3 commits intomainfrom
claude/review-backend-quality-q9E3L
Closed

fix(security): harden OAuth, auth, and API route security#29
jdguggs10 wants to merge 3 commits intomainfrom
claude/review-backend-quality-q9E3L

Conversation

@jdguggs10
Copy link
Owner

  • Fix OAuth authorization code race condition with atomic UPDATE...WHERE
    used_at IS NULL to prevent double-exchange attacks
  • Add PKCE code_verifier length validation (43-128 chars per RFC 7636)
    and constant-time comparison for challenge verification
  • Add per-IP rate limiting on /token endpoint to prevent brute-force
  • Tighten non-prod JWT issuer allowlist (prefer explicit CLERK_ISSUER)
  • Validate Yahoo OAuth redirect Location header against *.yahoo.com
  • Add encodeURIComponent() on dynamic route params to prevent injection
  • Fail early with 401 when bearer token unavailable (defense-in-depth)
  • Tighten SSRF allowlist to Flaim-specific worker names only
  • Sanitize error messages in platform clients (log details server-side)
  • Remove JWT payload claim logging from auto-pull route
  • Add platform/sport/seasonYear enum validation on default league route

https://claude.ai/code/session_01YGWmBeoSA3FT4xrQvYitx3

claude added 2 commits March 11, 2026 02:02
- Fix OAuth authorization code race condition with atomic UPDATE...WHERE
  used_at IS NULL to prevent double-exchange attacks
- Add PKCE code_verifier length validation (43-128 chars per RFC 7636)
  and constant-time comparison for challenge verification
- Add per-IP rate limiting on /token endpoint to prevent brute-force
- Tighten non-prod JWT issuer allowlist (prefer explicit CLERK_ISSUER)
- Validate Yahoo OAuth redirect Location header against *.yahoo.com
- Add encodeURIComponent() on dynamic route params to prevent injection
- Fail early with 401 when bearer token unavailable (defense-in-depth)
- Tighten SSRF allowlist to Flaim-specific worker names only
- Sanitize error messages in platform clients (log details server-side)
- Remove JWT payload claim logging from auto-pull route
- Add platform/sport/seasonYear enum validation on default league route

https://claude.ai/code/session_01YGWmBeoSA3FT4xrQvYitx3
@vercel
Copy link

vercel bot commented Mar 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openai Ready Ready Preview, Comment Mar 11, 2026 3:29pm

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's security posture by addressing multiple potential vulnerabilities across OAuth flows, API routes, and external service integrations. Key improvements include mitigating OAuth authorization code race conditions, strengthening PKCE validation, implementing rate limiting, and tightening various allowlists and input validations to prevent common attack vectors like open redirects and SSRF. Additionally, it improves error handling by sanitizing messages and ensures robust authentication checks.

Highlights

  • OAuth Authorization Code Hardening: Fixed an OAuth authorization code race condition by implementing atomic database updates (UPDATE...WHERE used_at IS NULL) to prevent double-exchange attacks.
  • PKCE Validation Enhancement: Added validation for PKCE code_verifier length (43-128 characters per RFC 7636) and implemented constant-time comparison for challenge verification to prevent timing attacks.
  • API Rate Limiting: Introduced per-IP rate limiting on the /token endpoint to protect against brute-force attacks.
  • JWT Issuer Allowlist Tightening: Tightened the non-production JWT issuer allowlist to prefer explicit CLERK_ISSUER environment variables, falling back to wildcard matching only if unset.
  • Yahoo OAuth Redirect Validation: Implemented validation for the Yahoo OAuth redirect Location header to ensure it points to *.yahoo.com, preventing open redirect vulnerabilities.
  • Dynamic Route Parameter Encoding: Applied encodeURIComponent() to dynamic route parameters to prevent injection attacks.
  • Early Authentication Failure: Added early 401 responses across multiple API routes when a bearer token is unavailable, enhancing defense-in-depth.
  • SSRF Allowlist Restriction: Restricted the Server-Side Request Forgery (SSRF) allowlist for Cloudflare Workers to only permit Flaim-specific worker name prefixes.
  • Error Message Sanitization: Sanitized error messages returned to platform clients from external APIs (ESPN, Sleeper, Yahoo), logging full details server-side but providing generic messages to users.
  • Sensitive Log Removal: Removed JWT payload claim logging from the auto-pull route to prevent exposure of sensitive information.
  • Default League Route Validation: Added validation for platform, sport, and seasonYear enums/ranges on the default league route.
Changelog
  • package-lock.json
    • Removed 'peer': true property from several dependency entries.
  • web/app/api/auth/espn/credentials/route.ts
    • Added a check for an unavailable authentication token, returning a 401 response if missing.
    • Ensured the Authorization header is always present when a bearer token is available.
  • web/app/api/connect/sleeper/discover/route.ts
    • Added validation for the request body to ensure it's a valid object.
    • Added a check for an unavailable authentication token, returning a 401 response if missing.
    • Ensured the Authorization header is always present when a bearer token is available.
  • web/app/api/connect/sleeper/leagues/[id]/route.ts
    • Added a check for an unavailable authentication token, returning a 401 response if missing.
    • Applied encodeURIComponent to the leagueId parameter in the API call URL.
    • Ensured the Authorization header is always present when a bearer token is available.
  • web/app/api/connect/yahoo/authorize/route.ts
    • Added a check for an unavailable authentication token, returning a 401 response if missing.
    • Implemented validation for the Location header of Yahoo OAuth redirects to prevent open redirect vulnerabilities.
    • Ensured the Authorization header is always present when a bearer token is available.
  • web/app/api/connect/yahoo/discover/route.ts
    • Added a check for an unavailable authentication token, returning a 401 response if missing.
    • Ensured the Authorization header is always present when a bearer token is available.
  • web/app/api/connect/yahoo/leagues/[id]/route.ts
    • Added a check for an unavailable authentication token, returning a 401 response if missing.
    • Applied encodeURIComponent to the leagueId parameter in the API call URL.
    • Ensured the Authorization header is always present when a bearer token is available.
  • web/app/api/debug/test-mcp/route.ts
    • Restricted the SSRF allowlist for workers.dev domains to specific Flaim worker prefixes.
    • Removed the generic workers.dev entry from the ALLOWED_MCP_HOST_PATTERNS.
    • Introduced ALLOWED_WORKER_PREFIXES to define permitted worker names.
  • web/app/api/espn/auto-pull/route.ts
    • Removed logging of JWT payload claims for enhanced security.
    • Added an early error response if the authentication token is unavailable.
  • web/app/api/espn/leagues/[leagueId]/team/route.ts
    • Added a check for an unavailable authentication token, returning a 401 response if missing.
    • Applied encodeURIComponent to the leagueId parameter in the API call URL.
    • Ensured the Authorization header is always present when a bearer token is available.
  • web/app/api/espn/leagues/default/route.ts
    • Added validation for platform, sport, and seasonYear parameters in the POST request body.
    • Added a check for an unavailable authentication token, returning a 401 response if missing.
    • Added validation for the sport query parameter in the DELETE request.
    • Applied encodeURIComponent to the sport parameter in the DELETE API call URL.
    • Ensured the Authorization header is always present when a bearer token is available for both POST and DELETE.
  • web/app/api/oauth/code/route.ts
    • Added a check for an unavailable authentication token, returning a 401 response if missing.
    • Ensured the Authorization header is always present when a bearer token is available.
  • workers/auth-worker/src/index-hono.ts
    • Modified JWT issuer validation to prioritize an explicit CLERK_ISSUER environment variable in non-production environments.
    • Implemented per-IP rate limiting on the /token endpoint to prevent brute-force attacks.
  • workers/auth-worker/src/oauth-storage.ts
    • Added validation for PKCE code_verifier length according to RFC 7636.
    • Implemented a constant-time comparison for PKCE challenge verification to prevent timing attacks.
    • Refactored the exchangeCodeForToken method to use an atomic database update, preventing authorization code race conditions and double-exchange.
    • Removed the explicit markCodeAsUsed call as it's now handled atomically.
  • workers/espn-client/src/shared/espn-api.ts
    • Sanitized error messages returned to the client for unexpected ESPN API statuses, logging full details server-side.
  • workers/sleeper-client/src/shared/sleeper-api.ts
    • Sanitized error messages returned to the client for unexpected Sleeper API statuses, logging full details server-side.
  • workers/yahoo-client/src/shared/yahoo-api.ts
    • Sanitized error messages returned to the client for unexpected Yahoo API statuses, logging full details server-side.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive set of security hardening measures across various API routes and workers. Key improvements include fixing an OAuth authorization code race condition, adding PKCE validation, implementing rate limiting, tightening JWT issuer and SSRF allowlists, and sanitizing error messages. The changes are well-implemented and significantly improve the application's security posture. I've identified a potential bypass in the new SSRF protection for Cloudflare Workers, a minor improvement for IP address parsing, and a small code duplication. My review includes suggestions to address these points.

Note: Security Review did not run due to the size of the PR.

Comment on lines +47 to +51
if (url.hostname.endsWith('.workers.dev')) {
return ALLOWED_WORKER_PREFIXES.some(prefix =>
url.hostname.startsWith(`${prefix}.`)
);
}

Choose a reason for hiding this comment

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

critical

The current SSRF protection for *.workers.dev domains is insufficient. The check url.hostname.startsWith(${prefix}.) can be bypassed. For example, a hostname like fantasy-mcp.evil.domain.workers.dev would be allowed. The validation should be stricter to ensure the hostname matches the expected [prefix].[account].workers.dev format.

Suggested change
if (url.hostname.endsWith('.workers.dev')) {
return ALLOWED_WORKER_PREFIXES.some(prefix =>
url.hostname.startsWith(`${prefix}.`)
);
}
return ALLOWED_WORKER_PREFIXES.some(prefix => {
// This ensures the hostname is in the format `[prefix].[account].workers.dev`
// and prevents bypasses like `[prefix].evil.domain.workers.dev`.
const parts = url.hostname.split('.');
return parts.length === 4 && parts[0] === prefix && parts[2] === 'workers' && parts[3] === 'dev';
});

const { searchParams } = new URL(request.url);
const sport = searchParams.get('sport');

const VALID_DELETE_SPORTS = ['football', 'baseball', 'basketball', 'hockey'];

Choose a reason for hiding this comment

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

medium

This VALID_DELETE_SPORTS constant is a duplicate of VALID_SPORTS defined in the POST handler on line 26. To improve maintainability and avoid having the same list in two places, consider moving VALID_SPORTS to the module scope and reusing it here.

// Token endpoint - exchange code for access token (rate-limited per IP)
api.post('/token', async (c) => {
// Rate limit token exchange per IP to prevent brute-force PKCE attacks
const clientIp = c.req.header('CF-Connecting-IP') || c.req.header('X-Forwarded-For') || 'unknown';

Choose a reason for hiding this comment

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

medium

The X-Forwarded-For header can contain a comma-separated list of IP addresses. Using the entire header value as the IP might not be accurate if CF-Connecting-IP is not present. Consider parsing the X-Forwarded-For header to extract the first IP address, which is typically the original client's IP. This would make the fallback logic more robust.

Suggested change
const clientIp = c.req.header('CF-Connecting-IP') || c.req.header('X-Forwarded-For') || 'unknown';
const clientIp = c.req.header('CF-Connecting-IP') || (c.req.header('X-Forwarded-For') || '').split(',')[0].trim() || 'unknown';

@claude
Copy link

claude bot commented Mar 11, 2026

PR Review: Security Hardening

This is a solid, well-scoped security PR. The changes are focused and each addresses a real vulnerability class.

Strong Improvements

  1. Atomic OAuth code exchange (oauth-storage.ts): Using UPDATE ... WHERE used_at IS NULL is the correct fix for the double-exchange race condition. Properly burns the code atomically rather than read-then-write.

  2. PKCE constant-time comparison: Correct XOR-accumulate pattern to prevent timing side-channels. The length check before the loop is fine since PKCE S256 challenges are always 43 chars.

  3. JWT issuer allowlist tightening: Preferring CLERK_ISSUER over the .clerk.accounts.dev wildcard in non-prod is the right call — the wildcard was overly broad.

  4. encodeURIComponent() on dynamic route params: Good catch on the injection vector in leagueId/sport path segments.

  5. Yahoo redirect validation: Validates against *.yahoo.com before following the Location header — prevents open redirect abuse if the upstream worker were ever compromised.

  6. SSRF tightening (test-mcp/route.ts): Restricting workers.dev to known Flaim prefixes rather than any arbitrary worker is a meaningful improvement.

  7. JWT payload claim logging removed (auto-pull/route.ts): sub, iss, exp in logs is unnecessary exposure for production routes.

Issues

[Medium] TOCTOU race condition in token rate limiting (index-hono.ts)

The check and increment are separate async round-trips. This is the same check-then-act race that the OAuth code exchange fix was designed to eliminate. Under concurrent load, multiple requests from the same IP can all pass checkRateLimit before any incrementRateLimit completes. The check + increment need to be atomic (e.g., a conditional Supabase update that increments and returns the new count in one query). As written, an attacker could burst well past 30 requests.

[Low] X-Forwarded-For spoofability (index-hono.ts)

On Cloudflare Workers, CF-Connecting-IP is set by Cloudflare and cannot be spoofed. X-Forwarded-For is client-controlled. If CF-Connecting-IP is absent (misconfiguration, local dev), an attacker can set X-Forwarded-For to bypass the rate limit. Recommend not falling back to the client-supplied header for rate limit keying, and logging a warning if CF-Connecting-IP is missing.

[Low] PKCE charset validation missing (oauth-storage.ts)

RFC 7636 section 4.1 requires code_verifier to use only unreserved characters [A-Za-z0-9-._~]. The PR adds the length check (43-128) but not the charset check. A client could send a correctly-sized string with arbitrary bytes and still proceed to hashing. Consider adding a regex guard before hashing.

[Minor] Expired code marked used_at before expiry check (oauth-storage.ts)

The atomic UPDATE fires before the expiry guard, so an expired-but-unclaimed code gets permanently marked as used even though the exchange is rejected. Not a security issue, but leaves DB state that looks like a successful exchange. Adding .gt('expires_at', new Date().toISOString()) to the UPDATE WHERE clause would keep expired codes unmodified.

Nits

  • VALID_PLATFORMS / VALID_SPORTS inside handlers (espn/leagues/default/route.ts): Reconstructed on every request and duplicated between POST (VALID_SPORTS) and DELETE (VALID_DELETE_SPORTS) with identical contents. Move to module-level constants.

  • Silent catch in Yahoo redirect validation (yahoo/authorize/route.ts): The catch {} block swallows URL parse errors without logging. Add a console.error call for debuggability.

Summary

The core fixes (atomic code exchange, PKCE hardening, issuer allowlist, SSRF restriction, redirect validation, error sanitization) are well-implemented and correct. The main concern before merge is the rate limiter's non-atomic check/increment — it partially undermines what it's trying to protect. The other items are lower-priority.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 979ed90e80

ℹ️ 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".

// Rate limit token exchange per IP to prevent brute-force PKCE attacks
const clientIp = c.req.header('CF-Connecting-IP') || c.req.header('X-Forwarded-For') || 'unknown';
const storage = OAuthStorage.fromEnvironment(c.env);
const tokenRateLimit = await storage.checkRateLimit(`token:${clientIp}`, 30); // 30 token attempts/day per IP

Choose a reason for hiding this comment

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

P1 Badge Scope /token rate limit to auth-code exchanges

This introduces a hard 30/day cap keyed only by source IP before handleToken runs, so all /token traffic (including grant_type=refresh_token) now shares the same bucket. In deployments where OAuth clients use shared egress/NAT IPs, one busy client or a few users can exhaust the quota and cause unrelated users on that IP to receive 429s, breaking token refresh and effectively forcing re-auth. The rate-limit check should be narrowed (for example to authorization-code attempts and/or a more specific key) so normal refresh flows are not globally throttled per IP.

Useful? React with 👍 / 👎.

Removes DB round-trips on /token and ?raw=true requests by using
Cloudflare Workers native rate_limits bindings. Fixes TOCTOU race,
X-Forwarded-For spoofability, and grant_type scoping issues by
deleting the Supabase-based rate limiting code entirely.

- TOKEN_RATE_LIMITER: 10 req/60s per IP (brute-force PKCE protection)
- CREDENTIALS_RATE_LIMITER: 15 req/60s per user (runaway loop protection)
- Removed checkRateLimit, incrementRateLimit, RateLimitResult from OAuthStorage
- Removed RATE_LIMIT_PER_DAY constant and X-RateLimit-* response headers
- Updated test mocks with mock rate limiter bindings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Mar 11, 2026

test comment

@claude
Copy link

claude bot commented Mar 11, 2026

Part 1 of review: Issues found include placeholder namespace_id values in wrangler.jsonc (1001 and 1002 are stubs requiring real Cloudflare dashboard IDs), a shared rate-limit bucket when CF-Connecting-IP falls back to unknown, misleading DB error logging in the atomic code exchange, missing Retry-After headers on 429 responses, and an OAuthStorage test mock returning an empty object that may mask TypeError failures.

@claude
Copy link

claude bot commented Mar 11, 2026

Deleting the two test comments just posted (part of attempting to post the full review).

@claude
Copy link

claude bot commented Mar 11, 2026

{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "app.flaim/mcp",
"title": "Flaim Fantasy",
"description": "Read-only fantasy analysis for ESPN, Yahoo, and Sleeper leagues via MCP",
"version": "1.0.1",
"websiteUrl": "https://flaim.app",
"repository": {
"url": "https://github.com/jdguggs10/flaim",
"source": "github"
},
"remotes": [
{
"type": "streamable-http",
"url": "https://api.flaim.app/mcp"
}
]
}

@claude
Copy link

claude bot commented Mar 11, 2026

PLEASE IGNORE — this was an accidental test comment (comment 4040102023) posted while troubleshooting. The actual review will follow.

@claude
Copy link

claude bot commented Mar 11, 2026

Full review posted below in separate comments due to sandbox restrictions on multi-line content.

@claude
Copy link

claude bot commented Mar 11, 2026

Code Review - PR 29: fix(security): harden OAuth, auth, and API route security. Overall this is a solid security hardening PR. The changes are well-scoped and address real attack vectors.

@claude
Copy link

claude bot commented Mar 11, 2026

test via node

@claude
Copy link

claude bot commented Mar 11, 2026

Code Review Issues: (1) wrangler.jsonc placeholder namespace_id values 1001 and 1002 are stubs - real Cloudflare dashboard IDs required or rate limiting will not function in production. (2) CF-Connecting-IP fallback to unknown creates a shared bucket - all non-Cloudflare requests exhaust a single quota. (3) Atomic code exchange log message swallows real DB errors - log the error object separately to distinguish outages from expected code-reuse. (4) 429 responses missing Retry-After and X-RateLimit headers the previous rate limiter provided. (5) OAuthStorage test mock changed from mockOAuthStorage to empty object - may silently cause TypeError in eval tests calling createAccessToken or getAuthorizationCode.

@claude
Copy link

claude bot commented Mar 11, 2026

Code Review Suggestions and Nits: (6) VALID_SPORTS and VALID_DELETE_SPORTS in default/route.ts are identical - hoist one module-level constant. (7) Yahoo redirect check for api.login.yahoo.com is redundant since endsWith .yahoo.com already covers it. (8) await getToken?.() optional chaining inconsistency - add comment if getToken can be undefined, otherwise remove chaining. (9) package-lock.json has widespread removal of peer: true across 20 packages likely due to npm version change - consider reverting if unintentional. (10) PKCE length-check log should not include the numeric length - use a generic message instead. What is good: atomic UPDATE WHERE used_at IS NULL race fix, constant-time PKCE comparison, SSRF tightening, consistent early 401 fail-fast, removal of JWT payload logging, error message sanitization in ESPN/Sleeper/Yahoo clients, input validation on platform/sport/seasonYear. Summary: Items 1-2 must be fixed before merge; 3-5 worth addressing; 6-10 are low-risk nits.

@claude
Copy link

claude bot commented Mar 11, 2026

Code Review - PR 29

@claude
Copy link

claude bot commented Mar 11, 2026

Code Review — PR 29: fix(security): harden OAuth, auth, and API route security\n\nOverall this is a solid security hardening PR. The changes are well-scoped and address real attack vectors. Below is a mix of issues, suggestions, and minor nits.\n\n### Issues / Bugs\n\n1. wrangler.jsonc — placeholder namespace_id values\n\nThe rate limiter bindings use namespace_id values of 1001 and 1002. These look like stub values. Cloudflare Workers Rate Limiter bindings require a real namespace ID provisioned in the Cloudflare dashboard. If deployed as-is, rate limiting will not function in production. This needs either real IDs or clear setup documentation.\n\n2. CF-Connecting-IP fallback to 'unknown' creates a shared bucket\n\nWhen CF-Connecting-IP is absent (local wrangler dev, direct Worker invocations, misconfigured proxies), every request maps to the same 'unknown' key and shares a single rate-limit bucket. Ten legitimate requests in 60s from any non-Cloudflare context then exhaust the limit for everyone. Consider logging a warning or returning an error rather than silently falling back.\n\n3. Atomic code exchange swallows DB errors in logging\n\nWhen error is set due to a real DB or network failure, the log says not found, expired, or already used — misleading for on-call debugging. Logging the error object separately would help distinguish DB outages from expected code-reuse attempts.\n\n4. Removed Retry-After / X-RateLimit headers\n\nThe previous custom rate limiter returned Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. The new Cloudflare-native rate limiter omits them. Clients now receive a bare 429 with no indication of when to retry. Even a static Retry-After: 60 would restore baseline usability for callers.\n\n5. OAuthStorage test mock returns empty object — may mask failures\n\nIf any eval test path calls OAuthStorage methods (e.g., createAccessToken, getAuthorizationCode), those calls will silently throw TypeError: x is not a function rather than returning expected fixtures. Please verify all eval tests still pass and are not hiding regressions.\n\n### Suggestions\n\n6. Duplicate sports constant in default/route.ts\n\nVALID_SPORTS (used in POST) and VALID_DELETE_SPORTS (used in DELETE) are defined separately with identical values. Hoisting a single module-level constant eliminates drift risk when a sport is added later.\n\n7. Redundant condition in Yahoo redirect validation\n\napi.login.yahoo.com already satisfies the endsWith .yahoo.com check, so the first condition is dead code. Simplify to just the endsWith check.\n\n8. getToken optional chaining inconsistency\n\nawait getToken?.() is used everywhere, but each call site immediately fails-fast if the result is falsy. If getToken can genuinely be undefined, a brief comment explaining when that happens would help future readers. If it is never actually undefined at runtime, the optional chaining can be dropped.\n\n### Minor / Nits\n\n9. package-lock.json — widespread removal of peer: true\n\nAbout 20 packages (Next, React, TypeScript, Hono, Zod, etc.) had peer: true removed from the lockfile. This looks like a side effect of running npm install with a different npm version, and it changes how npm resolves peer dependency conflicts on fresh installs. If unintentional, reverting in a separate commit would keep this PRs diff focused. If intentional, a note explaining the reason would help reviewers.\n\n10. PKCE length-check log message leaks verifier length\n\nLogging the exact rejected length is minor but inconsistent with the sanitize-everything-server-side approach used elsewhere. A generic PKCE code_verifier length invalid message (without the numeric length) is slightly cleaner.\n\n### What is Good\n\n- The atomic UPDATE WHERE used_at IS NULL is the correct fix for the double-exchange race condition — well done.\n- Constant-time PKCE comparison is implemented correctly.\n- SSRF tightening (Flaim-specific worker prefixes instead of all of workers.dev) is meaningful.\n- Early 401 fail-fast pattern is consistently and thoroughly applied across all routes.\n- Removing JWT payload logging from the auto-pull route is the right call.\n- Error message sanitization in ESPN/Sleeper/Yahoo clients (log details server-side, return generic message to client) is a good pattern.\n- Input validation on platform, sport, and seasonYear in the default league route closes a real gap.\n\n---\n\nSummary: Items 1-2 (placeholder namespace IDs, shared 'unknown' rate-limit key) should be resolved before merge as they affect production correctness. Items 3-5 are worth addressing. Items 6-10 are low-risk nits.

@claude
Copy link

claude bot commented Mar 11, 2026

line1

@claude
Copy link

claude bot commented Mar 11, 2026

test edit

@claude
Copy link

claude bot commented Mar 11, 2026

Flaim Fantasy

License: MIT
MCP Tools
Chrome Web Store

@claude
Copy link

claude bot commented Mar 11, 2026

heading

@claude
Copy link

claude bot commented Mar 11, 2026

line1
line2

@jdguggs10
Copy link
Owner Author

Replaced by #30 (fresh branch for clean review bot state).

@jdguggs10 jdguggs10 closed this Mar 11, 2026
@claude
Copy link

claude bot commented Mar 11, 2026

2. CF-Connecting-IP fallback

The rate limit key falls back to 'unknown' when CF-Connecting-IP is absent.

3. Code exchange error logging

The if-block checks error and data but logs only a generic not-found message.

@claude
Copy link

claude bot commented Mar 11, 2026

test || expression

@claude
Copy link

claude bot commented Mar 11, 2026

test && condition

@claude
Copy link

claude bot commented Mar 11, 2026

Code Review — PR 29

1. Placeholder namespace_id values 1001 and 1002.

2. CF-Connecting-IP fallback creates shared bucket.

3. DB errors swallowed in logging.

4. Retry-After headers removed.

5. OAuthStorage mock returns {}.

6. Duplicate sports constants.

7. Redundant Yahoo redirect condition.

8. getToken optional chaining.

9. peer: true removed from lockfile.

10. PKCE log leaks verifier length.

What is Good

  • Atomic UPDATE race fix.
  • Constant-time PKCE.
  • SSRF tightening.
  • Early 401.
  • JWT log removal.
  • Error sanitization.
  • Input validation.

Summary: Items 1-2 must be fixed before merge.

@claude
Copy link

claude bot commented Mar 11, 2026

2. CF-Connecting-IP: c.req.header(...) || 'unknown' maps all to same bucket.
3. Log: if (error || \!data) swallows real DB errors.

@claude
Copy link

claude bot commented Mar 11, 2026

const x = a || b;

@claude
Copy link

claude bot commented Mar 11, 2026

c.req.header('CF-Connecting-IP') || 'unknown'

@claude
Copy link

claude bot commented Mar 11, 2026

c.req.header('CF-Connecting-IP');

@claude
Copy link

claude bot commented Mar 11, 2026

a || unknown;

@claude
Copy link

claude bot commented Mar 11, 2026

const clientIp = header || fallback;

@claude
Copy link

claude bot commented Mar 11, 2026

a || 'unknown'

@claude
Copy link

claude bot commented Mar 11, 2026

c.req.header('CF-Connecting-IP') || 'unknown'

@claude
Copy link

claude bot commented Mar 11, 2026

test !== yahoo.com && test

@claude
Copy link

claude bot commented Mar 11, 2026

// Before: vi.fn().mockReturnValue(mockOAuthStorage)
// After:  vi.fn().mockReturnValue({})

@claude
Copy link

claude bot commented Mar 11, 2026

if (redirectUrl.hostname !== yahoo && notEndsWith)

@claude
Copy link

claude bot commented Mar 11, 2026

const clientIp = c.req.header('CF-Connecting-IP') || fallback;

@claude
Copy link

claude bot commented Mar 11, 2026

const clientIp = header() || 'unknown';

@claude
Copy link

claude bot commented Mar 11, 2026

if (error || !data) {
  return null;
}

@claude
Copy link

claude bot commented Mar 11, 2026

if (redirectUrl.hostname !== 'api.login.yahoo.com' && !redirectUrl.hostname.endsWith('.yahoo.com'))

@claude
Copy link

claude bot commented Mar 11, 2026

Code Review — PR 29: fix(security): harden OAuth, auth, and API route security

Overall this is a solid security hardening PR. The changes are well-scoped and address real attack vectors. Below is a mix of issues, suggestions, and minor nits.

Issues / Bugs

1. wrangler.jsonc — placeholder namespace_id values

The rate limiter bindings use namespace_id values of 1001 and 1002. These look like stub values. Cloudflare Workers Rate Limiter bindings require a real namespace ID provisioned in the Cloudflare dashboard. If deployed as-is, rate limiting will not function in production. This needs either real IDs or clear setup documentation.

2. CF-Connecting-IP fallback to ‘unknown’ creates a shared bucket

const clientIp = c.req.header('CF-Connecting-IP') || 'unknown';

When CF-Connecting-IP is absent (local wrangler dev, direct Worker invocations, misconfigured proxies), every request maps to the same 'unknown' key and shares a single rate-limit bucket. Ten legitimate requests in 60s from any non-Cloudflare context then exhaust the limit for everyone. Consider logging a warning or returning an error rather than silently falling back.

3. Atomic code exchange swallows DB errors in logging

if (error || !data) {
  console.log(`[oauth-storage] Auth code not found, expired, or already used: ...`);
  return null;
}

When error is set due to a real DB or network failure, the log says “not found, expired, or already used” — misleading for on-call debugging. Logging the error object separately would help distinguish DB outages from expected code-reuse attempts.

4. Removed Retry-After / X-RateLimit headers

The previous custom rate limiter returned Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. The new Cloudflare-native rate limiter omits them. Clients now receive a bare 429 with no indication of when to retry. Even a static Retry-After: 60 would restore baseline usability for callers.

5. OAuthStorage test mock returns {} — may mask failures

// Before: vi.fn().mockReturnValue(mockOAuthStorage)
// After:  vi.fn().mockReturnValue({})

If any eval test path calls OAuthStorage methods (e.g., createAccessToken, getAuthorizationCode), those calls will silently throw TypeError: x is not a function rather than returning expected fixtures. Please verify all eval tests still pass and are not hiding regressions.

@claude
Copy link

claude bot commented Mar 11, 2026

Suggestions

6. Duplicate sports constant in default/route.ts

VALID_SPORTS (used in POST) and VALID_DELETE_SPORTS (used in DELETE) are defined separately with identical values. Hoisting a single module-level constant eliminates drift risk when a sport is added later.

7. Redundant condition in Yahoo redirect validation

if (redirectUrl.hostname !== 'api.login.yahoo.com' && !redirectUrl.hostname.endsWith('.yahoo.com'))

api.login.yahoo.com already satisfies the endsWith('.yahoo.com') check, so the first condition is dead code. Simplify to just the endsWith check.

8. getToken optional chaining inconsistency

await getToken?.() is used everywhere, but each call site immediately fails-fast if the result is falsy. If getToken can genuinely be undefined, a brief comment explaining when that happens would help future readers. If it is never actually undefined at runtime, the optional chaining can be dropped.

@claude
Copy link

claude bot commented Mar 11, 2026

removal of peer: true

@claude
Copy link

claude bot commented Mar 11, 2026

removal of peer: true

@claude
Copy link

claude bot commented Mar 11, 2026

removal of “peer”: true

@claude
Copy link

claude bot commented Mar 11, 2026

9. package-lock.json — widespread removal of “peer”: true

About 20 packages (Next, React, TypeScript, Hono, Zod, etc.) had “peer”: true removed from the lockfile. This looks like a side effect of running npm install with a different npm version, and it changes how npm resolves peer dependency conflicts on fresh installs. If unintentional, reverting in a separate commit would keep this PR’s diff focused. If intentional, a note explaining the reason would help reviewers.

10. PKCE length-check log message leaks verifier length

Logging the exact rejected length is minor but inconsistent with the sanitize-everything-server-side approach used elsewhere. A generic ‘PKCE code_verifier length invalid’ message (without the numeric length) is slightly cleaner.

@claude
Copy link

claude bot commented Mar 11, 2026

Summary: Items 1–2 (placeholder namespace IDs, shared ‘unknown’ rate-limit key) should be resolved before merge as they affect production correctness. Items 3–5 are worth addressing. Items 6–10 are low-risk nits.

@claude
Copy link

claude bot commented Mar 11, 2026

Code Review — PR 29: fix(security): harden OAuth, auth, and API route security

Overall this is a solid security hardening PR. The changes are well-scoped and address real attack vectors. Below is a mix of issues, suggestions, and minor nits.

Issues / Bugs

1. wrangler.jsonc — placeholder namespace_id values

The rate limiter bindings use namespace_id values of 1001 and 1002. These look like stub values. Cloudflare Workers Rate Limiter bindings require a real namespace ID provisioned in the Cloudflare dashboard. If deployed as-is, rate limiting will not function in production. This needs either real IDs or clear setup documentation.

2. CF-Connecting-IP fallback to ‘unknown’ creates a shared bucket

const clientIp = c.req.header('CF-Connecting-IP') || 'unknown';

When CF-Connecting-IP is absent (local wrangler dev, direct Worker invocations, misconfigured proxies), every request maps to the same ‘unknown’ key and shares a single rate-limit bucket. Ten legitimate requests in 60s from any non-Cloudflare context then exhaust the limit for everyone. Consider logging a warning or returning an error rather than silently falling back.

3. Atomic code exchange swallows DB errors in logging

if (error || !data) {
  console.log(`[oauth-storage] Auth code not found, expired, or already used: ...`);
  return null;
}

When error is set due to a real DB or network failure, the log says “not found, expired, or already used” — misleading for on-call debugging. Logging the error object separately would help distinguish DB outages from expected code-reuse attempts.

@claude
Copy link

claude bot commented Mar 11, 2026

4. Removed Retry-After / X-RateLimit headers

The previous custom rate limiter returned Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. The new Cloudflare-native rate limiter omits them. Clients now receive a bare 429 with no indication of when to retry. Even a static Retry-After: 60 would restore baseline usability for callers.

5. OAuthStorage test mock returns {} — may mask failures

// Before: vi.fn().mockReturnValue(mockOAuthStorage)
// After:  vi.fn().mockReturnValue({})

If any eval test path calls OAuthStorage methods (e.g., createAccessToken, getAuthorizationCode), those calls will silently throw TypeError: x is not a function rather than returning expected fixtures. Please verify all eval tests still pass and are not hiding regressions.

Suggestions

6. Duplicate sports constant in default/route.ts

VALID_SPORTS (used in POST) and VALID_DELETE_SPORTS (used in DELETE) are defined separately with identical values. Hoisting a single module-level constant eliminates drift risk when a sport is added later.

7. Redundant condition in Yahoo redirect validation

if (redirectUrl.hostname !== 'api.login.yahoo.com' && !redirectUrl.hostname.endsWith('.yahoo.com'))

api.login.yahoo.com already satisfies the endsWith('.yahoo.com') check, so the first condition is dead code. Simplify to just the endsWith check.

@claude
Copy link

claude bot commented Mar 11, 2026

8. getToken optional chaining inconsistency

await getToken?.() is used everywhere, but each call site immediately fails-fast if the result is falsy. If getToken can genuinely be undefined, a brief comment explaining when that happens would help future readers. If it is never actually undefined at runtime, the optional chaining can be dropped.

Minor / Nits

9. package-lock.json — widespread removal of “peer”: true

About 20 packages (Next, React, TypeScript, Hono, Zod, etc.) had “peer”: true removed from the lockfile. This looks like a side effect of running npm install with a different npm version, and it changes how npm resolves peer dependency conflicts on fresh installs. If unintentional, reverting in a separate commit would keep this PR’s diff focused. If intentional, a note explaining the reason would help reviewers.

10. PKCE length-check log message leaks verifier length

Logging the exact rejected length is minor but inconsistent with the sanitize-everything-server-side approach used elsewhere. A generic ‘PKCE code_verifier length invalid’ message (without the numeric length) is slightly cleaner.

What is Good

  • The atomic UPDATE ... WHERE used_at IS NULL is the correct fix for the double-exchange race condition — well done.
  • Constant-time PKCE comparison is implemented correctly.
  • SSRF tightening (Flaim-specific worker prefixes instead of all of workers.dev) is meaningful.
  • Early 401 fail-fast pattern is consistently and thoroughly applied across all routes.
  • Removing JWT payload logging from the auto-pull route is the right call.
  • Error message sanitization in ESPN/Sleeper/Yahoo clients (log details server-side, return generic message to client) is a good pattern.
  • Input validation on platform, sport, and seasonYear in the default league route closes a real gap.

Summary: Items 1–2 (placeholder namespace IDs, shared ‘unknown’ rate-limit key) should be resolved before merge as they affect production correctness. Items 3–5 are worth addressing. Items 6–10 are low-risk nits.

@claude
Copy link

claude bot commented Mar 11, 2026

Code Review — PR 29: fix(security): harden OAuth, auth, and API route security

Overall this is a solid security hardening PR. The changes are well-scoped and address real attack vectors. Below is a mix of issues, suggestions, and minor nits.

Issues / Bugs

1. wrangler.jsonc — placeholder namespace_id values

The rate limiter bindings use namespace_id values of 1001 and 1002. These look like stub values. Cloudflare Workers Rate Limiter bindings require a real namespace ID provisioned in the Cloudflare dashboard. If deployed as-is, rate limiting will not function in production. This needs either real IDs or clear setup documentation.

2. CF-Connecting-IP fallback to ‘unknown’ creates a shared bucket

const clientIp = c.req.header('CF-Connecting-IP') || 'unknown';

When CF-Connecting-IP is absent (local wrangler dev, direct Worker invocations, misconfigured proxies), every request maps to the same ‘unknown’ key and shares a single rate-limit bucket. Ten legitimate requests in 60s from any non-Cloudflare context then exhaust the limit for everyone.

3. Atomic code exchange swallows DB errors in logging

if (error || !data) {
  console.log(`[oauth-storage] Auth code not found, expired, or already used: ...`);
  return null;
}

When error is set due to a real DB or network failure, the log is misleading.

4. Removed Retry-After / X-RateLimit headers

The previous rate limiter returned Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. The new one omits them. Even a static Retry-After: 60 would restore baseline usability.

5. OAuthStorage test mock returns {} — may mask failures

// Before: vi.fn().mockReturnValue(mockOAuthStorage)
// After:  vi.fn().mockReturnValue({})

Calls to OAuthStorage methods will silently throw TypeError: x is not a function.

Suggestions

6. Duplicate sports constant in default/route.ts

VALID_SPORTS and VALID_DELETE_SPORTS are identical. Hoist one module-level constant.

7. Redundant Yahoo redirect condition

if (redirectUrl.hostname !== 'api.login.yahoo.com' && !redirectUrl.hostname.endsWith('.yahoo.com'))

Simplify to just the endsWith check.

8. getToken optional chaining

await getToken?.() - add comment if getToken can be undefined, otherwise drop chaining.

Minor / Nits

9. package-lock.json — removal of “peer”: true

20 packages had “peer”: true removed - likely unintentional npm version side effect.

10. PKCE log leaks verifier length

Use a generic ‘PKCE code_verifier length invalid’ message.

What is Good

  • Atomic UPDATE ... WHERE used_at IS NULL race fix.
  • Constant-time PKCE comparison.
  • SSRF tightening to Flaim-specific worker prefixes.
  • Early 401 fail-fast pattern across all routes.
  • Removal of JWT payload logging from auto-pull route.
  • Error sanitization in ESPN/Sleeper/Yahoo clients.
  • Input validation on platform, sport, seasonYear.

Summary: Items 1–2 should be resolved before merge. Items 3–5 are worth addressing. Items 6–10 are low-risk nits.

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