aws-samples / aws-samples/sample-agentcore-launchpad

Memory ownership not validated: spec-pinned memory_id grants cross-app AgentCore memory access; lifecycle API allows MEMBER deletion of unmanaged resources

Open
#55 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
5
Forks
0
Avg merge
54m
Merged PRs (30d)
82

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:

1. **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-276` grants `CreateEvent / GetEvent / ListEvents / ListSessions / ListActors / RetrieveMemoryRecords / GetMemoryRecord / ListMemoryRecords` on `arn:...: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:*` on `Resource: "*"` (`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/agents` only requires `perm:agents.deploy`, which is granted by default.

2. **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`:

```text
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)

```python
"""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/resources` and `DELETE` are `MEMBER` with no perm gate, unlike the `perm:eval.run` precedent 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

1. 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 status `ACTIVE`.
2. 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".
3. Gate `POST`/`DELETE /api/memory/resources` behind a revocable perm key (e.g. `perm:memory.admin`), consistent with `perm: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.

Contributor guide

Open the contributing guide

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.

Assessment

Tech stack
aws, python
Domain
authorization, backend-api-design, cloud, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.