T09 · Insecure Skill Coding Practices
- Location
scripts/whoop_auth.py:78- Finding
OAuth Callback Accepts Authorization Codes Without State Validation
- Content
View full analysis
Vulnerability Details
File Location:
scripts/whoop_auth.py, lines 78–115
Vulnerability Type: OAuth login CSRF and account confusion caused by missingstatevalidation
Risk Level: MediumVulnerable Code:
python def run_auth_flow(client_id, client_secret): state = secrets.token_urlsafe(16) auth_code_holder = {} class CallbackHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): parsed = urllib.parse.urlparse(self.path) if parsed.path == "/callback": params = urllib.parse.parse_qs(parsed.query) auth_code_holder["code"] = params.get("code", [None])[0] auth_code_holder["error"] = params.get("error", [None])[0] self.send_response(200) self.send_header("Content-Type", "text/html") self.end_headers() self.wfile.write(b"<h2>Authorization complete. You can close this tab.</h2>") def log_message(self, format, *args): pass server = http.server.HTTPServer(("localhost", 8080), CallbackHandler) thread = threading.Thread(target=server.handle_request) thread.start() params = urllib.parse.urlencode({ "client_id": client_id, "redirect_uri": REDIRECT_URI, "response_type": "code", "scope": SCOPES, "state": state, })Technical Analysis
The authentication flow generates a cryptographically random OAuth
statevalue and includes it in the authorization request. However, the callback handler only extractscodeanderror; it neither retrieves the returnedstateparameter nor compares it with the value generated for the active authentication flow.OAuth
statebinds an authorization response to the browser session that initiated it. Merely sending the parameter does not provide protection. The callback must reject resp ...[truncated 2339 chars]- Remediation
View remediation
Remediation Suggestions
- Extract the callback's
statevalue and require an exact match with the locally generated value before accepting either an authorization code or an OAuth error. - Use
secrets.compare_digest()for the comparison and reject missing or multi-valued state parameters. - Return an HTTP error response for mismatched callbacks and continue waiting for a valid callback until the authentication timeout expires.
- Accept only one valid callback and explicitly close the HTTP server after completion or timeout.
- Add PKCE using an S256 code challenge and verifier so an injected or intercepted authorization code cannot be redeemed without the locally generated verifier.
- Avoid placing unnecessary authorization details in console output, and clearly report rejected callback attempts without logging codes or tokens.
Example hardening:
python returned_state = params.get("state", [None])[0] expected_state = state if ( returned_state is None or not secrets.compare_digest(returned_state, expected_state) ): self.send_response(400) self.send_header("Content-Type", "text/plain") self.end_headers() self.wfile.write(b"Invalid OAuth state.") return code_values = params.get("code", []) if len(code_values) != 1 or not code_values[0]: self.send_response(400) self.end_headers() return auth_code_holder["code"] = code_values[0]- Extract the callback's
