Memory ownership not validated: spec-pinned memory_id grants cross-app AgentCore memory access; lifecycle API allows MEMBER deletion of unmanaged resources
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 35/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- aws, python
- Domain
- authorization, backend-api-design, cloud, security
Research direction
Start with tests/test_audit_poc_memory_ownership.py and run the three hermetic tests with the repository's standard harness. Trace memory_id through backend/app/schemas/agent.py, agent_iam.py, deployer/harness.py, deployer/environment.py, and the memory resource routers and services. Done means foreign or unmanaged memories cannot be read, pinned, or deleted by an ordinary member, while valid platform-owned resources retain the intended lifecycle behavior.
Written by the indexing model from the issue text.
Description
Summary
While reviewing 9179b25 (memory resource management + per-agent memory selection) and 6b33f59, we found that memory ownership is never validated on two new surfaces introduced by these commits:
-
Per-agent memory selection (
spec.memory.memory_id) trusts member input. The only constraint is a format regex (backend/app/schemas/agent.py:75-77). The id then flows — without any "belongs to this workspace / was created by this platform" check — into:- agent execution-role IAM:
backend/app/services/agent_iam.py:257-276grantsCreateEvent / GetEvent / ListEvents / ListSessions / ListActors / RetrieveMemoryRecords / GetMemoryRecord / ListMemoryRecordsonarn:...:memory/{selected_memory}; - runtime binding:
backend/app/deployer/harness.py:236-243+backend/app/deployer/environment.py:29-32(agentCoreMemoryConfiguration,LAUNCHPAD_MEMORY_ID); - console read-back:
backend/app/routers/chat.py:309-316,backend/app/services/observability.py:1597-1652.
Since the workspace spoke role holds
bedrock-agentcore:*onResource: "*"(infra/spoke/launchpad-workspace-role.yaml:278-286— comment: "the boundary here is that this role only reaches THIS account"), any member can point an agent at any AgentCore Memory in the spoke account — including memories created by other teams/tools outside Launchpad — and read/pollute their records. Members can even ship their own runtime code (spec.code/spec.code_bundle,schemas/agent.py:196-199) and call those IAM permissions directly.POST /api/agentsonly requiresperm:agents.deploy, which is granted by default. - agent execution-role IAM:
-
The memory lifecycle API has no ownership checks either. All four new routes are classified
MEMBER(backend/app/core/route_policy.py:264-271).GET/DELETE /api/memory/resources/{memory_id}(backend/app/routers/memory_resources.py:137-160) accept arbitrary ids. The only protections on delete (backend/app/services/memory_admin.py:234-243+ the router's in-use check) cover just the calling workspace's own bootstrap memory, and the in-use scan only sees agents of the calling workspace. Deletion is irreversible (AgentCore deletes all events and memory records).
Reproduction
Drop the attached test into backend/tests/ and run with the repo's standard harness — all three tests pass on 6b33f59:
tests/test_audit_poc_memory_ownership.py
test_foreign_memory_is_listed_with_full_metadata PASSED
test_foreign_memory_delete_passes_every_guard PASSED
test_pin_guard_disappears_when_pinning_agent_is_deleted PASSED
PoC test source (test_audit_poc_memory_ownership.py)
"""Audit PoC (commits 9179b25 + 6b33f59): no ownership validation on memory resources.
Demonstrates that the memory-resource lifecycle API trusts the AWS account
boundary as if it were the workspace boundary: a memory that the platform did
not create and no platform agent pins (e.g. another team's memory in the same
spoke account) is listed with full metadata and deleted by a plain member
session — only the calling workspace's own bootstrap memory is refused.
"""
import pytest
import app.services.memory_admin as ma
from app.core.db import SessionLocal
from app.models.ledger import Agent
from .conftest import set_default_resources
from .test_memory_resources import StubControl, MEM_ID
FOREIGN = "analytics_team_prod-AAAA" # not platform-created, nothing pins it
@pytest.fixture
def configured(client):
set_default_resources({"memory_id": MEM_ID})
def wire(monkeypatch, control):
monkeypatch.setattr(ma, "control_client", lambda _ws=None: control)
def test_foreign_memory_is_listed_with_full_metadata(client, configured, monkeypatch):
control = StubControl(
memories=[
{"id": MEM_ID, "arn": "arn:mem:1", "status": "ACTIVE"},
{"id": FOREIGN, "arn": "arn:mem:foreign", "status": "ACTIVE"},
]
)
wire(monkeypatch, control)
body = client.get("/api/memory/resources")
assert body.status_code == 200
items = {m["id"]: m for m in body.json()["items"]}
assert items[FOREIGN]["arn"] == "arn:mem:foreign"
assert items[FOREIGN]["is_default"] is False
assert items[FOREIGN]["agents"] == [] # and nothing stops what follows
def test_foreign_memory_delete_passes_every_guard(client, configured, monkeypatch):
control = StubControl(memories=[{"id": FOREIGN, "arn": "arn:mem:foreign"}])
wire(monkeypatch, control)
res = client.delete(f"/api/memory/resources/{FOREIGN}")
assert res.status_code == 200
assert res.json() == {"deleted": True, "id": FOREIGN}
assert ("delete_memory", {"memoryId": FOREIGN}) in control.calls
def test_pin_guard_disappears_when_pinning_agent_is_deleted(
client, configured, monkeypatch
):
"""The 'referenced by a live agent' guard reads current spec pins; a member
with the default-granted perm:agents.delete can delete the pinning agent,
after which the same DELETE succeeds."""
control = StubControl(memories=[{"id": FOREIGN, "arn": "arn:mem:foreign"}])
wire(monkeypatch, control)
db = SessionLocal()
agent = Agent(
workspace_id="default", name="pinner", method="zip_runtime", status="active",
spec={"memory": {"short_term": True, "memory_id": FOREIGN}},
)
db.add(agent)
db.commit()
db.close()
pinned = client.delete(f"/api/memory/resources/{FOREIGN}")
assert pinned.status_code == 409 # guard fires while the pin exists
assert not any(op == "delete_memory" for op, _ in control.calls)
db = SessionLocal()
row = db.get(Agent, agent.id)
row.status = "deleted" # what PERM_AGENT_DELETE leaves behind
db.commit()
db.close()
again = client.delete(f"/api/memory/resources/{FOREIGN}")
assert again.status_code == 200
assert ("delete_memory", {"memoryId": FOREIGN}) in control.calls
The tests use the repo's own hermetic fixtures: a memory not created by the platform and not pinned by any agent (simulating another team's resource in the same spoke account) is listed with full metadata, deleted successfully by a plain member session, and the "pin protection" disappears as soon as the pinning agent is deleted (members hold perm:agents.delete by default).
Impact
- Shared spoke account (multiple teams / non-Launchpad resources in the account): a plain member can read and pollute other applications' AgentCore memory (conversation records, preferences — typically PII-adjacent) via their own agent runtime, and irreversibly delete any memory in the account. In that deployment shape this is effectively cross-tenant access on the data plane.
- Single-team dedicated account: degrades to members being able to point agents at (and irreversibly delete) arbitrary platform memories other than the bootstrap one, with no revocable permission key —
POST /api/memory/resourcesandDELETEareMEMBERwith no perm gate, unlike theperm:eval.runprecedent for billable/state-changing AWS calls.
The delete-protection also has a timing gap: the in-use check reads the current spec, while a running runtime keeps the deploy-time-baked LAUNCHPAD_MEMORY_ID (environment.py:29-32) — unpinning without a re-deploy makes the guard blind to a live reference.
Suggested fixes
- Validate ownership when a spec pins
memory_id(create and re-deploy): accept only ids returned by the workspace's RESOURCES listing / resource map, and require statusACTIVE. - Ownership-filter the lifecycle API: list/show/delete only platform-created memories (
launchpad_memory-*prefix or resource-map registration); render external resources read-only as "detected, not managed". - Gate
POST/DELETE /api/memory/resourcesbehind a revocable perm key (e.g.perm:memory.admin), consistent withperm:agents.delete/perm:eval.run.
Notes
- Both commits are credited (nice hermetic contract tests and input validation on namespace keys — no issues found there).
- Found during a security review of the two commits; nothing was executed against AWS infrastructure — all verification is local (fixtures + IAM-policy review). Happy to provide more detail or test PRs.
- The report deliberately states the deployment-shape caveats above: on a dedicated single-team account with only platform-created memories, the practical severity is lower.
- Dominant language
- Python
- Stars
- 14
- Forks
- 2
- Avg merge
- 2h 6m
- Merged PRs (30d)
- 136
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from aws-samples/sample-agentcore-launchpad
-
Difficulty 3/5 Half a day Newbie friendliness 58/100
aws-samples/sample-agentcore-launchpad#279 ·
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 72/100
aws-samples/sample-agentcore-launchpad#67 ·
Maintainers usually reply within 1 day
All issues in aws-samples/sample-agentcore-launchpad
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
ok-oldking/ok-wuthering-waves#1785 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
healthchecks/healthchecks#1349 ·
-
deployment release-lag
Difficulty 1/5 Under an hour Newbie friendliness 68/100
nolte/kamerplanter#2374 ·
Maintainers usually reply within 1 day
-
bug v1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
modelcontextprotocol/python-sdk#3725 ·
Maintainers usually reply within 1 day
-
batchterms() drops queries without a similar() clause, misaligning results with inputsPossibly taken @MohammadHijjawi97 claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
Maintainers usually reply within 1 day