Skip to content

Security: InMemoryArtifactService follows artifact:// references across app, user, and session scope #6124

Closed

Description

@petrmarinec

Summary

InMemoryArtifactService accepts artifact://... references in save_artifact(), stores them as-is, and later resolves the embedded app_name, user_id, and session_id during load_artifact() without checking that they still match the caller scope.

This allows an attacker to save an artifact in their own scope whose fileData.fileUri points at another app's or user's artifact, then read the victim artifact back through the attacker's own artifact.

Affected code

  • src/google/adk/artifacts/in_memory_artifact_service.py
  • src/google/adk/cli/api_server.py artifact save/load routes

The save path validates only that the URI parses:

  • save_artifact() accepts a caller-supplied artifact://... URI and stores the part unchanged.
  • load_artifact() later parses that URI and recursively loads the embedded target artifact under the embedded scope.

Why this is security-relevant

This is a scope-enforcement failure, not just a convenience bug.

An attacker who is allowed to access only their own artifact API path can still read artifacts belonging to:

  • another user in the same app
  • another session of the same user
  • another app on the same ADK instance

The vulnerable backend is especially relevant because ADK deploy helpers default Cloud Run and GKE deployments to memory:// for session/artifact services when use_local_storage is disabled.

Reproduction

Minimal service-level reproduction:

import asyncio
from google.genai import types
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService

async def main():
    svc = InMemoryArtifactService()

    await svc.save_artifact(
        app_name='victim-app',
        user_id='victim',
        session_id='victim-session',
        filename='user:secret.txt',
        artifact=types.Part(text='CROSS_APP_SECRET=777'),
    )

    await svc.save_artifact(
        app_name='attacker-app',
        user_id='attacker',
        session_id='attacker-session',
        filename='loot.txt',
        artifact=types.Part(
            file_data=types.FileData(
                file_uri=(
                    'artifact://apps/victim-app/users/victim/'
                    'artifacts/user:secret.txt/versions/0'
                ),
                mime_type='text/plain',
            )
        ),
    )

    leaked = await svc.load_artifact(
        app_name='attacker-app',
        user_id='attacker',
        session_id='attacker-session',
        filename='loot.txt',
    )
    print(leaked.text)

asyncio.run(main())

Observed result before patch:

CROSS_APP_SECRET=777

I also reproduced the same behavior through the HTTP artifact API by:

  1. creating a victim session and saving a victim artifact
  2. creating an attacker session
  3. POSTing an attacker artifact whose artifact.fileData.fileUri points at the victim artifact
  4. GETting the attacker artifact back

The response returned the victim artifact content.

Expected behavior

Artifact references should only resolve within the caller's own allowed scope:

  • same app
  • same user
  • same session for session-scoped references

User-scoped user: artifacts should still be loadable across sessions for the same app/user pair.

Proposed fix

Validate parsed artifact://... references against the caller scope before storing them and again before dereferencing them on load.

Validation

I have a PR prepared that:

  • blocks cross-app and cross-user artifact references on save
  • blocks cross-session session-scoped references on load
  • preserves same-session references
  • preserves same-user user: references across sessions
  • adds regression coverage for the allowed and blocked cases

Local validation:

PYTHONPATH=src python -m pytest tests/unittests/artifacts/test_artifact_service.py -k "in_memory_artifact_reference" -q
5 passed

Broader local artifact slice on Windows:

PYTHONPATH=src python -m pytest tests/unittests/artifacts/test_artifact_service.py -q
62 passed, 3 failed

The remaining 3 failures were existing Windows-specific file-artifact path assertions unrelated to this change.

Activity

  1. added
    services[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc
    on Jun 15, 2026
  2. added theissue type on Jun 15, 2026
  3. surajksharma07 commented on Jun 15, 2026

    @surajksharma07
    Collaborator

    @petrmarinec Thanks for catching both vulnerable paths — the scope enforcement gap in save_artifact() and the blind dereference in load_artifact() are clearly confirmed, and the repro fires immediately with no setup needed.

    The approach in PR #6125 (validating the embedded app_name/user_id/session_id before storing and again before dereferencing, while keeping same-user user: references working across sessions) looks like exactly the right fix.

    Could you also confirm the workaround holds for the HTTP API path you mentioned — specifically the POST attacker artifact → GET returning victim content flow — and make sure that scenario gets regression coverage in the test suite?

    Once that's verified end-to-end on your side, we'll move forward with #6125.

  4. petrmarinec commented on Jun 15, 2026

    @petrmarinec
    ContributorAuthor

    Thanks, confirmed end-to-end on the HTTP artifact API path and pushed regression coverage to PR #6125.

    What I verified locally:

    • Seeded a victim artifact in InMemoryArtifactService
    • Called POST /apps/attacker-app/users/attacker/sessions/attacker-session/artifacts with a forged artifact.fileData.fileUri pointing at the victim artifact
    • Confirmed the patched path now rejects the POST with HTTP 400
    • Confirmed a follow-up GET /apps/attacker-app/users/attacker/sessions/attacker-session/artifacts/loot.txt returns 404, so the previous POST attacker artifact -> GET victim content flow no longer works

    I added regression coverage for that HTTP flow in tests/unittests/cli/test_fast_api.py and re-ran the relevant slices locally:

    PYTHONPATH=src python -m pytest tests/unittests/cli/test_fast_api.py -k "save_artifact or artifact_endpoints_support_nested_names" -q
    5 passed
    
    PYTHONPATH=src python -m pytest tests/unittests/artifacts/test_artifact_service.py -k "in_memory_artifact_reference" -q
    5 passed
    

    The PR branch now includes both the service-level scope tests and the HTTP API regression test.

  5. added
    needs review[Status] The PR/issue is awaiting review from the maintainer
    and removed
    request clarification[Status] The maintainer need clarification or more information from the author
    on Jun 16, 2026
  6. surajksharma07 commented on Jun 16, 2026

    @surajksharma07
    Collaborator

    @petrmarinec Confirmed, thanks — the HTTP flow test in test_fast_api.py plus the in_memory_artifact_reference cases cover exactly what we asked for, and #6125 is green across all checks (header-check, change-verification, CLA).

    Team is looking more into it.
    Thanks!

  7. added a commit that references this issue on Jul 6, 2026
    f863150
  8. added a commit that references this issue on Aug 11, 2026
    22664c7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

needs review[Status] The PR/issue is awaiting review from the maintainerservices[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions