
I Found a $90k Remote Code Execution Vulnerability in Google
Writeup of an unauthenticated RCE on Google's shared DevSite platform (developers.google.com, firebase.google.com, developer.android.com and others). The Flask session cookie embedded a pickled OAuth2WebServerFlow containing the OAuth client_secret, which was also the Flask SECRET_KEY. That let the author forge signed sessions carrying a malicious pickle that the OAuth callback unpickled; RCE was proven safely with time.sleep timing and negative controls. It earned a $90,000 bounty and lists a four-part fix.
What it is
An unauthenticated remote code execution in Google's shared DevSite platform. A single Flask session key signed valid sessions on developers.google.com, firebase.google.com, developer.android.com, source.android.com and docs.partner.android.com. The root cause is a Flask SECRET_KEY that is the same value as an OAuth client_secret, and that secret is embedded in the cookie it signs.
How it works
GET /oauth2authorize?return_url=%2Fneeds no account. It sets a ~1,200-byte Flask session cookie.- The cookie is only signed, not encrypted. A leading
.on the payload means zlib compression. Decoding it showsgoogle_oauth2_flow_<csrf>holding a base64 value tagged" b"(Flask's bytes marker). - That value is a pickled
oauth2client.client.OAuth2WebServerFlowcontainingclient_idandclient_secretin plaintext. - The
client_secretis also the Flask signing key. The author recomputed the signature offline and it matched the issued cookie byte for byte:
derived_key = hmac.digest(client_secret, b"cookie-session", "sha1")
expected = hmac.digest(
derived_key,
f"{payload}.{timestamp}".encode(),
"sha1",
)- The OAuth callback verifies the signature, reads the flow entry, base64-decodes it and calls
pickle.loads. With the key known, an attacker forges a session with an arbitrary CSRF token and a matching flow key holding a malicious pickle. Thestateparameter is attacker-controlled, so nothing has to match server-side.
flowchart LR A[Cookie leaks client_secret] --> C[Forge signed session] B[client_secret = SECRET_KEY] --> C C --> D[Callback pickle.loads] D --> E[RCE]
The author avoided running commands on production. The pickle's __reduce__ returned time.sleep(N), and response time tracked N:
| N | Response |
|---|---|
| 0 | 0.207 s |
| 3 | 3.193 s |
| 5 | 5.194 s |
| 8 | 8.283 s |
Negative controls:
- No cookie, or one flipped signature byte:
Invalid request. - Valid signature with no flow entry:
Invalid request state. - Valid signature with the real flow: reached the token exchange and failed with
invalid_grant.
Preconditions / Affected
- The app signs sessions with a key that is exposed or derivable.
- The app unpickles session-held data on a reachable endpoint.
- Here, both held on every DevSite deployment sharing the key. The page says the secret was still live when the post was written, but the issue is reported as fixed.
What to do
For your own Flask or OAuth apps, the page lists four fixes. Any one breaks the chain:
- Rotate both exposed OAuth client secrets and invalidate every session signed with them.
- Give each deployment its own high-entropy
SECRET_KEYfrom a server-side store, unrelated to any OAuth credential. - Keep the OAuth flow object server-side. The cookie should carry only an opaque reference.
- Don't use pickle across a client boundary. Use a data-only format with schema validation so a forged cookie is inert.
A quick audit follows from the write-up: decode your own session cookies, because Flask cookies are readable by anyone holding one. Look for secrets, pickled blobs, or long payloads. Then check whether the signing key is derived from, or equal to, any value that appears in them.
Caveats
- Code execution was inferred from timing and controls, not from running commands on Google's servers.
- The page doesn't give the report or fix dates, and the timeline entries carry no timestamps.
- The reward was $90,000: $75,000 base for S0 RCE in a Normal Google Application (T2), times 1.2 for report quality.
Organisations named here
- DevSite
- pickle deserialization
- REDUCE opcode
- session forgery
Technologies named here
- curl
- Google DevSite platform
- Python
- developers.google.com
- firebase.google.com
- pickletools.dis
- Python hmac
- developer.android.com
- source.android.com
- itsdangerous
- docs.partner.android.com
- Python pickle with a __reduce__ time.sleep payload
- Flask session cookies signed via itsdangerous
- Google Bug Hunters
- Google OAuth client_id and client_secret exposed in cookie
- Flask
- OAuth
Techniques named here
- Information disclosure
- Offline signature recomputation
- Insecure deserialization
- Time-based blind proof of RCE with time.sleep
- Negative controls
- Cross-domain impact
- Reward
Related in AppSec
Flowise MCP Security Bypass Enables RCE
Inside an Oracle Database SQL Injection Attack | Huntress
OASIS
