Summary
Two code paths in ADK Python call pickle.loads() on data read from the session database with no type restriction, integrity check, or opt-in gate. A single poisoned row in events.actions (BLOB column) achieves arbitrary code execution in any ADK process that reads the session — including the official migration tool.
Affected Code
Sink 1 — Runtime read path
src/google/adk/sessions/schemas/v0.py:97 — DynamicPickleType.process_result_value()
if dialect.name in ("spanner+spanner", "mysql"):
return pickle.loads(value) # no guards
SQLite falls through to PickleType.result_processor() which calls self.pickler.loads(value) — same effect. All three supported dialects (SQLite, MySQL, Cloud Spanner) are affected.
Sink 2 — Migration path
src/google/adk/sessions/migration/migrate_from_sqlalchemy_pickle.py:62
if isinstance(actions_val, bytes):
actions = pickle.loads(actions_val) # no guards
This runs on every existing row during the recommended v0→v1 migration (adk migrate session). The vendor remediation path is itself an exploit trigger on a poisoned database.
Affected Versions
All google-adk releases through current (v1.31.1).
< 1.22.0 — v0 (pickle) was the only schema
1.22.0+ — v1 (JSON) is the default for new databases; existing v0 databases continue using the pickle path (docs confirm)
The pickle code path is not deprecated, not warning-gated, and not behind a feature flag.
Impact
- RCE in any ADK process reading events from a v0-schema database
- Blast radius: one poisoned row compromises every reader until the row is manually deleted
- Persistence: payload survives process restarts, container rebuilds, and ADK version upgrades
- Cross-authority: write-authority (e.g., dev environment, adjacent service with DB access) and execution-authority (production ADK process) can be distinct principals — the database is treated as a trust boundary that pickle collapses
- Migration trap: running
adk migrate session on a poisoned DB executes every planted payload under the production service account
Suggested Remediation
- Fail-closed v0 reads (preferred): refuse to load v0-schema databases unless explicitly opted in via
ADK_ALLOW_UNSAFE_V0_PICKLE=1. Forces operators to acknowledge risk rather than inheriting it silently.
- RestrictedUnpickler: if v0 must remain loadable, allowlist only
EventActions and known field types. Apply to both Sink 1 and Sink 2.
- HMAC on serialized data before storage as defense-in-depth.
- Long-term: deprecate v0, remove
v0.py.
CVSS 3.1
9.9 Critical — AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
(S:U fallback: 8.8 High)
Disclosure
- Reported to Google VRP: 2026-04-24
- GHSA filed
- 90-day coordinated disclosure deadline: 2026-07-23
- This issue is filed per Google VRP guidance to work with maintainers via public issues
Discovered by SPR{K}3 Security Research (Dan Aridor — @daridor9)
Summary
Two code paths in ADK Python call
pickle.loads()on data read from the session database with no type restriction, integrity check, or opt-in gate. A single poisoned row inevents.actions(BLOB column) achieves arbitrary code execution in any ADK process that reads the session — including the official migration tool.Affected Code
Sink 1 — Runtime read path
src/google/adk/sessions/schemas/v0.py:97—DynamicPickleType.process_result_value()SQLite falls through to
PickleType.result_processor()which callsself.pickler.loads(value)— same effect. All three supported dialects (SQLite, MySQL, Cloud Spanner) are affected.Sink 2 — Migration path
src/google/adk/sessions/migration/migrate_from_sqlalchemy_pickle.py:62This runs on every existing row during the recommended v0→v1 migration (
adk migrate session). The vendor remediation path is itself an exploit trigger on a poisoned database.Affected Versions
All
google-adkreleases through current (v1.31.1).< 1.22.0— v0 (pickle) was the only schema1.22.0+— v1 (JSON) is the default for new databases; existing v0 databases continue using the pickle path (docs confirm)The pickle code path is not deprecated, not warning-gated, and not behind a feature flag.
Impact
adk migrate sessionon a poisoned DB executes every planted payload under the production service accountSuggested Remediation
ADK_ALLOW_UNSAFE_V0_PICKLE=1. Forces operators to acknowledge risk rather than inheriting it silently.EventActionsand known field types. Apply to both Sink 1 and Sink 2.v0.py.CVSS 3.1
9.9 Critical —
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H(S:U fallback: 8.8 High)
Disclosure
Discovered by SPR{K}3 Security Research (Dan Aridor — @daridor9)