✓ Human-authored analysis; AI used for formatting and proofreading. Your Cognito app client has http://localhost:3000/callback in its allowed callback URLs. It was added during development. Nobody removed it. That URL is now a token exfiltration endpoint. An attacker crafts an authorization link that redirects the OAuth response containing the user's access token, ID token, and refresh token to any server running on localhost. On a compromised workstation, a shared development machine, or any environment where the attacker can bind to port 3000, every token issued by your application is interceptable. OWASP ranks open redirect and OAuth misconfiguration in its Top 10. Bug bounty platforms pay consistently for callback URL findings. The fix takes thirty seconds. The misconfiguration has been live since your first sprint. How the attack works OAuth 2.0 authorization code flow with Cognito: 1. User clicks "Login with Google/Facebook/SSO" 2. Browser redirects to Cognito's /authorize endpoint 3. User authenticates with the identity provider 4. Cognito redirects to the callback URL with the authorization code 5. App exchanges the code for tokens Step 4 is where the vulnerability lives. Cognito checks that the redirect URI matches one of the app client's allowed callback URLs. If http://localhost:3000/callback is in the list, this redirect is valid: https://your-app.auth.us-east-1.amazoncognito.com/authorize? client_id=abc123& response_type=code& redirect_uri=http://localhost:3000/callback& scope=openid+email+profile The user sees a legitimate login page. They authenticate. Cognito sends the authorization code to http://localhost:3000/callback. If the attacker controls what's listening on port 3000 of the user's machine, they receive the code. They exchange it for tokens. They have the user's session. With implicit grant flow (still enabled on many app clients), it's worse. The tokens are in the URL fragment directly. No code exchange needed: http://localhost:3000/callback#access_token=eyJhbG...&id_token=eyJhbG... The attacker's listener on localhost receives the tokens in the URL. No additional request to Cognito. Instant session hijack. The five callback URL mistakes The localhost case is the most common, but not the only dangerous pattern: 1. http://localhost:3000/callback Development URL left in production configuration. Exploitable on any machine where the attacker can bind to the port such as compromised workstations, shared dev servers, CI runners with exposed ports. 2. http://localhost:* or wildcard port patterns Some teams add multiple localhost variants (ports 3000, 3001, 8080) or use patterns. Each port is an additional listener the attacker can target. 3. https://*.example.com/callback Wildcard subdomain matching. If the attacker can create any subdomain (via subdomain takeover on a decommissioned S3 bucket, Route 53 dangling CNAME, or a compromised development environment), they can register attacker.example.com and receive the redirect. 4. https://example.com/callback without HTTPS enforcement If the app client allows http:// callbacks alongside https://, a network-level attacker (coffee shop wifi, compromised router) can intercept the redirect before it reaches the legitimate server. 5. https://staging.example.com/callback Staging environment callback left in the production app client. If staging has weaker access controls, logging, or monitoring, the attacker redirects production tokens to the staging endpoint and extracts them from staging logs or staging's weaker database protections. What your scanner checks Every cloud security scanner checks the Cognito app client individually: "Is the client secret enabled?" — Maybe. Public clients (SPAs, mobile apps) don't use client secrets. "Are OAuth scopes restricted?" — Depends on the app's needs. "Is the callback URL configured?" — Yes, it's configured. The scanner sees a URL is present. No scanner asks: "Does the callback URL list contain entries that would allow an attacker to redirect tokens to a server they control?" That's not a boolean check. It requires understanding which URL patterns are safe (production HTTPS endpoints) and which are dangerous (localhost, wildcards, HTTP, staging environments). The compound pattern The callback URL misconfiguration alone is a vulnerability. Combined with other app client settings, it becomes a chain: Token theft + no token revocation: App client has EnableTokenRevocation = false. Once the attacker has the tokens, the legitimate user can't invalidate them. The attacker's session persists until the tokens expire which, with a refresh token, can be indefinitely. Token theft + long-lived tokens: App client has AccessTokenValidity = 24 hours and RefreshTokenValidity = 365 days. The attacker's window for using stolen tokens expands from minutes to a year. Token theft + implicit grant enabled: App client has AllowedOAuthFlows = ["implicit"]. Tokens appear directly in the URL with no code exchange needed. The attack is simpler and harder to detect because there's no server-side code exchange to log. Token theft + broad scopes: App client has AllowedOAuthScopes = ["openid", "email", "profile", "aws.cognito.signin.user.admin"]. The aws.cognito.signin.user.admin scope allows the token holder to call Cognito user management APIs — read user attributes, change email, change password. Token theft escalates to account takeover. Each setting is individually configurable. The compound consisting of open redirect + no revocation + long-lived tokens + admin scope creates a scenario where one stolen token gives the attacker persistent, privileged, irrevocable access to the user's account and potentially to backend AWS resources via the identity pool's role mapping. What compound detection finds Running this configuration through static analysis that checks interactions across app client settings: Individual findings: [HIGH] Callback URL includes localhost entry [MEDIUM] Token revocation disabled [MEDIUM] Access token validity exceeds 1 hour [LOW] Implicit grant flow enabled alongside authorization code flow [MEDIUM] Admin user scope included in allowed scopes Five individual findings, three different severity levels. A security team triaging by severity works the HIGH first and queues the MEDIUMs for next sprint. Compound finding: [CRITICAL] Open redirect token theft chain Callback URL allows attacker-controlled redirect + tokens are irrevocable + token lifetime is 24 hours + admin scope grants account management = persistent privileged account takeover Fix: Remove localhost from callback URLs ($0, 30 seconds) Impact: Blocks the entire chain at step 1 One compound finding, CRITICAL severity, with the fix that costs nothing and blocks the entire chain. The compound view changes the prioritization: this isn't a MEDIUM that can wait for next sprint. It's a CRITICAL that blocks with a 30-second fix. The fix # List your app clients aws cognito-idp list-user-pool-clients \ --user-pool-id <pool-id> \ --max-results 20 # For each client, check callback URLs aws cognito-idp describe-user-pool-client \ --user-pool-id <pool-id> \ --client-id <client-id> \ | jq '.UserPoolClient.CallbackURLs' # Remove dangerous entries aws cognito-idp update-user-pool-client \ --user-pool-id <pool-id> \ --client-id <client-id> \ --callback-urls "https://app.example.com/callback" Remove every callback URL that isn't a production HTTPS endpoint. Specifically: Remove all http://localhost* entries Remove all http:// entries (non-HTTPS) Remove all wildcard subdomain entries unless you control DNS for the entire domain Remove all staging/development environment entries from production app clients Separate staging and production app clients if they don't exist as separate clients already Then fix the compound: # Enable token revocation aws cognito-idp update-user-pool-client \ --user-pool-id <pool-id> \ --client-id <client-id> \ --enable-token-revocation # Reduce token lifetime aws cognito-idp update-user-pool-client \ --user-pool-id <pool-id> \ --client-id <client-id> \ --access-token-validity 60 \ --token-validity-units '{"AccessToken":"minutes"}' # Remove admin scope if not needed aws cognito-idp update-user-pool-client \ --user-pool-id <pool-id> \ --client-id <client-id> \ --allowed-o-auth-scopes "openid" "email" "profile" The callback URL fix blocks the chain entirely. The other fixes are defense in depth. They reduce the impact if a different token exfiltration vector is found in the future. How to check your own environment # Scan all app clients across all user pools for dangerous callback URLs for pool in $(aws cognito-idp list-user-pools --max-results 60 \ | jq -r '.UserPools[].Id'); do for client in $(aws cognito-idp list-user-pool-clients \ --user-pool-id "$pool" --max-results 60 \ | jq -r '.UserPoolClients[].ClientId'); do callbacks=$(aws cognito-idp describe-user-pool-client \ --user-pool-id "$pool" --client-id "$client" \ | jq -r '.UserPoolClient.CallbackURLs[]?' 2>/dev/null) for url in $callbacks; do case "$url" in http://localhost*) echo "DANGEROUS: $pool/$client → $url (localhost)" ;; http://*) echo "WARNING: $pool/$client → $url (non-HTTPS)" ;; *\**) echo "WARNING: $pool/$client → $url (wildcard)" ;; esac done done done If any line prints DANGEROUS, you have a live token exfiltration endpoint in your production OAuth configuration. The thirty-second rule This vulnerability has been in your configuration since the first developer added http://localhost:3000/callback to test the OAuth flow. It's been there for months or years. The fix is removing one URL from a list. It takes thirty seconds. The compound consisting of open redirect + irrevocable tokens + admin scope escalates a thirty-second fix into a career-defining incident. The difference between "we removed a dev URL" and "we had a persistent privileged account takeover chain in production for eighteen months" is whether someone checked the interaction between five app client settings that each looked reasonable on their own. Your scanner checked each one. It said four of five were fine. The fifth was a MEDIUM. The compound is a CRITICAL. The fix is thirty seconds. The scenarios in this article are modeled on real configurations found in bug bounty programs and OAuth security assessments. The analysis uses Stave, an open-source static analysis tool that evaluates cloud configurations via CEL predicates and exports standardized facts for consumption by external reasoning engines from air-gapped snapshots without cloud credentials.