T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/whoop_auth.py:78
- Finding
- OAuth Callback Accepts Authorization Codes Without State Validation## Vulnerability Details **File Location**: `scripts/whoop_auth.py`, lines 78–115 **Vulnerability Type**: OAuth login CSRF and account confusion caused by missing `state` validation **Risk Level**: Medium **Vulnerable 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 `state` value and includes it in the authorization request. However, the callback handler only extracts `code` and `error`; it neither retrieves the returned `state` parameter nor compares it with the value generated for the active authentication flow. OAuth `state` binds 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
- ## Remediation Suggestions 1. Extract the callback's `state` value and require an exact match with the locally generated value before accepting either an authorization code or an OAuth error. 2. Use `secrets.compare_digest()` for the comparison and reject missing or multi-valued state parameters. 3. Return an HTTP error response for mismatched callbacks and continue waiting for a valid callback until the authentication timeout expires. 4. Accept only one valid callback and explicitly close the HTTP server after completion or timeout. 5. Add PKCE using an S256 code challenge and verifier so an injected or intercepted authorization code cannot be redeemed without the locally generated verifier. 6. 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] ```
