T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/gdrive_sa.py:97
- Finding
- Unvalidated Service-Account Token URI Enables SSRF and Disclosure of Signed Authentication Material<![CDATA[ ## Vulnerability Details **File Location**: `scripts/gdrive_sa.py`, lines 97-132 **Vulnerability Type**: Unvalidated credential-controlled network destination / SSRF **Risk Level**: Medium ### Vulnerable Code ```python def mint_access_token(sa: dict, scope: str, subject: str | None) -> str: now = int(time.time()) header = {"alg": "RS256", "typ": "JWT"} claim = { "iss": sa["client_email"], "scope": scope, "aud": sa.get("token_uri", "https://oauth2.googleapis.com/token"), "iat": now, "exp": now + 3600, } if subject: claim["sub"] = subject signing_input = f"{b64url(json.dumps(header, separators=(',', ':')).encode())}.{b64url(json.dumps(claim, separators=(',', ':')).encode())}" signature = sign_rs256(signing_input.encode("ascii"), sa["private_key"]) assertion = f"{signing_input}.{b64url(signature)}" payload = urllib.parse.urlencode( { "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": assertion, } ).encode("utf-8") req = urllib.request.Request( sa.get("token_uri", "https://oauth2.googleapis.com/token"), data=payload, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST", ) try: with urllib.request.urlopen(req) as resp: data = json.loads(resp.read().decode("utf-8")) except urllib.error.HTTPError as exc: detail = exc.read().decode("utf-8", "replace") raise SystemExit(f"Token exchange failed: HTTP {exc.code}: {detail}") return data["access_token"] ``` ### Technical Analysis The service-account JSON is loaded from `GOOGLE_SERVICE_ACCOUNT_KEY`, which may either contain JSON directly or identify a local JSON file. Its optional `token_uri` property is used without validation as: 1. The `aud` claim of a newly signed JWT assertion. 2. The destination of an outbound HTTP POST request. No check restric ...[truncated 2570 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Do not trust `token_uri` from externally supplied service-account JSON. Use a fixed token endpoint: ```python GOOGLE_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token" ``` Use this constant for both the JWT `aud` claim and the outbound request URL. 2. If alternate endpoints are operationally required, implement a strict allowlist based on parsed URL components: - Require the `https` scheme. - Require an explicitly approved hostname. - Require the expected path. - Reject embedded credentials, fragments, unexpected ports, and malformed URLs. - Do not use suffix-only hostname checks that could accept attacker-controlled domains. 3. Prevent redirects from moving token requests to an unapproved origin. Either disable automatic redirects for token exchange or validate every redirect destination against the same allowlist. 4. Fail closed when endpoint validation fails and avoid including the JWT assertion or credentials in error messages or logs. 5. Treat the service-account JSON as security-sensitive configuration: - Restrict file permissions. - Do not accept credential files from untrusted workspaces or uploads. - Validate expected service-account fields before use. - Document that control of this configuration is equivalent to control of the authentication flow. A fixed-endpoint implementation should resemble: ```python GOOGLE_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token" claim = { "iss": sa["client_email"], "scope": scope, "aud": GOOGLE_TOKEN_ENDPOINT, "iat": now, "exp": now + 3600, } req = urllib.request.Request( GOOGLE_TOKEN_ENDPOINT, data=payload, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST", ) ``` ]]>
