-
Notifications
You must be signed in to change notification settings - Fork 579
[New Advisory] pyporscheconnectapi — Missing PKCE, static OAuth state, and credential exposure (CWE-352, CWE-732) #7308
Copy link
Copy link
Open
Description
Package Information
- Ecosystem: pip
- Package name: pyporscheconnectapi
- Repository: https://github.com/CJNE/pyporscheconnectapi
- Affected versions: <= latest
- Patched versions: None
- Severity: Medium
- CWE: CWE-352, CWE-732
Summary
pyporscheconnectapi (66 stars, Porsche Connect API) has no PKCE in its OAuth2 flow, uses a hardcoded static state parameter, caches tokens to world-readable files, and logs access tokens at debug level.
Finding 1: No PKCE + Hardcoded Static State (CWE-352, High)
oauth2.py line 159 — the authorization request has no code_challenge/code_verifier and uses a static state:
"state": "pyporscheconnectapi", # hardcoded, never validated on callback
This is a public client (no client_secret) with custom URI scheme redirect (my-porsche-app://auth0/callback). Without PKCE, intercepted auth codes are exchangeable for tokens. The static state defeats CSRF protection. RFC 7636 requires PKCE for public clients.
Finding 2: Token Cached to World-Readable File (CWE-732, High)
cli.py line 248 writes full OAuth2 token to .session with no chmod:
async with aiofiles.open(session_file, "w", encoding="utf-8") as f:
await f.write(json.dumps(token, ensure_ascii=False, indent=2))
Default permissions (0644). Any local user can read the Porsche refresh token.
Finding 3: Access Tokens and Auth Codes Logged (CWE-532, Medium)
oauth2.py logs full tokens and auth codes at DEBUG level:
Line 125: _LOGGER.debug("Refreshed Access Token: %s", token.access_token)
Line 131: _LOGGER.debug("New Access Token: %s", token.access_token)
Lines 166, 186, 201: _LOGGER.debug("Authorization code: %s", authorization_code)
Finding 4: Plaintext Password in Config File (CWE-256, Medium)
cli.py line 306-313 reads password from .porscheconnect.cfg with no permission checks:
config.read([".porscheconnect.cfg", Path("~/.porscheconnect.cfg").expanduser()])
parser.add_argument("-p", "--password", default=config.get("porsche", "password"))
Finding 5: Shared Mutable Default httpx Client (CWE-665, Medium)
connection.py line 41: async_client=httpx.AsyncClient() — mutable default evaluated once. Multiple Porsche accounts share cookies, causing potential cross-account session bleed.
Fix PR
https://github.com/CJNE/pyporscheconnectapi/pull/89
Affected code
No PKCE + static state: https://github.com/CJNE/pyporscheconnectapi/blob/main/pyporscheconnectapi/oauth2.py#L159
Token file: https://github.com/CJNE/pyporscheconnectapi/blob/main/pyporscheconnectapi/cli.py#L248
Token logging: https://github.com/CJNE/pyporscheconnectapi/blob/main/pyporscheconnectapi/oauth2.py#L125
Config password: https://github.com/CJNE/pyporscheconnectapi/blob/main/pyporscheconnectapi/cli.py#L306
Shared client: https://github.com/CJNE/pyporscheconnectapi/blob/main/pyporscheconnectapi/connection.py#L41Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Type
Fields
Give feedbackNo fields configured for issues without a type.