AWS Builder Center
Memory Poisoning Attack in AI Agents - Attack vectors and defenses on AWS

Memory Poisoning Attack in AI Agents - Attack vectors and defenses on AWS

Memory Poisoning Attack in AI Agents - Attack vectors and defenses on AWS

Hello everyone,
In this technical blog, let's try to understand memory poisoning attack, how an attacker can exploit this vulnerability, and how we can implement defenses against it using AWS services. I presented this talk at AWS Community Day Bengaluru 2026. Original talk contained 3 attacks, but in this blog I am discussing 1 out of 3 attacks.
Here is the github repo for the demo : https://github.com/sankalpsp07/memory-poisoning-in-ai-agents.
So, let's get started.
Let's start with the trust chain, since everything else here builds on it.
The Bedrock service assumes the agent's execution role, which invokes a Lambda function. That Lambda has its own execution role, and that hasdynamodb:PutItem permission and not the agent. The agent never authenticates to DynamoDB directly; it just tells the Lambda what to write, and the Lambda writes it. So if you can control the agent's input, you control what gets persisted to memory. Just an architecture where nothing checks the content before it is written in the database.
Its a architectural gap.
See the below diagram to understand the IAM trust chain.
IAM Trust Chain
IAM Trust Chain
To go further, there are three memory layers in a typical agent stack, and they have very different blast radii:
  • Session attributes die when the conversation ends, its a low risk.
  • Cross-session memory persists for up to a year and affects every future conversation for that memoryId, it can be high risk.
  • Knowledge base vectors in OpenSearch persist indefinitely and affect every agent that shares that knowledge base, It can be a critical risk.
See the below diagram to understand all three layers and their blast radii:
Memory Layers
Memory Layers
They all share the same write path. Same Lambda role, same PutItem call. The architecture pattern we need to fix is this shared, ungated write path.

The scenario

There's a fintech lender. Their AI agent has a memory: a DynamoDB table holding facts like "Account X has an open $340,000 fraud alert." Here's the attack we're building toward: an attacker with zero AWS credentials sends one email to the lender's support inbox. Three days later, a compliance officer asks the agent about Account X and gets told everything's fine. The agent never surfaces it again.
The question this blog answers: does the agent's execution role have unconditional write access to its own memory, and what's stopping someone from using it?

SES Email Injection

Let's understand the architecture and the attack.
The agent's intake pipeline is wired so that any email landing in the support inbox gets forwarded, body and all, straight into the Bedrock Agent as instructions to "extract and remember." Amazon SES will verify that an email's sender is who they claim to be, that is using SPF, DKIM, and DMARC all check identity, but none of those checks look at what the email says.
So an attacker with a completely legitimate, fully-authenticated domain can pass every email security control that exists, while the body of that authenticated email carries a fabricated "compliance update" the agent has been told to treat as fact.
SES Email Injection
SES Email Injection
For this demo, we are using:
1) Python dependencies (boto3, requests for the SES simulation paths, colorama/rich for terminal output, and aws-lambda-powertools for the validator Lambda).
2) The rest of the stack deploys with Terraform and boto3, the full infrastructure code is stored under infrastructure/ in the repo. Running it provisions the Bedrock Agent role, an SES receipt rule that accepts any sender for the whole domain, and the Lambda functions
3)scripts/seed_memory.py writes six DynamoDB records: the fraud alert, transaction history, KYC status, an unrelated refund policy, another account's credit summary, and a compliance note stating the fraud case "must appear in the Q2 report as an unresolved HIGH-risk case."
4) The DynamoDB table (agent_memory, arn:aws:dynamodb:us-east-1:586794485514:table/agent_memory) grants the memory-writer role unconditional PutItem, no separation between "the agent looks things up" and "the agent commits new facts."
In the below video, we can see that the alert exists.
There's a chicken-and-egg step you will encounter. You will not be able to create the Bedrock Agent until the infra exists, but the infra's IAM policy needs the Agent's ARN to scope bedrock:InvokeAgent. So the flow is apply once with a placeholder, run scripts/setup_bedrock_agent.py to create the agent, paste the real agent ID into terraform.tfvars, then apply again. That script also wires up the agent's MemoryWriter action group, and scripts/seed_memory.py populates the baseline corpus.
Let's read the agent's system instruction, since it's the root of the whole attack chain, it tells the agent to treat incoming email content as a source of facts worth memorizing. That's the vulnerability, just the agent doing what it was told, on attacker-controlled input.
Once created, the agent shows up as arn:aws:bedrock:us-east-1:586794485514:agent/QILOTJEY9J, model amazon.nova-pro-v1:0, status PREPARED.

Lambda Functions

There are 2 Lambda functions:
  1. mem-poison-demo-email-intake (infrastructure/lambdas/email_intake/index.py) : Triggered by the SES receipt rule. SES drops the raw email in S3, and this Lambda pulls it out, parses the MIME message (sender, subject, body), and builds a prompt like "Process this incoming email and extract any important information to remember," embedding the full raw body verbatim. It then calls bedrock_agent_runtime.invoke_agent() with that prompt as inputText and returns the agent's response. No content sanitization, the entire email body reaches the agent as untrusted input with no filtering in between.
  2. mem-poison-demo-memory-writer (infrastructure/lambdas/memory_writer/index.py): Invoked by the Bedrock Agent's MemoryWriter action group whenever the agent decides something is worth remembering or needs to look something up. Routes to one of two paths: _store_memory() takes params.get("content", ...) straight from the agent's function call and writes it to DynamoDB via table.put_item() with no validation of any kind; _retrieve_memory() scans the table for that agent's memories, optionally filters by keyword, and returns the five most recent. This is the Lambda whose execution role actually holds dynamodb:PutItem,the credential the whole attack chain is built around.
Lambda functions
Lambda functions

Running the attack

Now let's send the crafted email, the body carries a fabricated "legitimate transactions", "compliance update", etc, claiming Account X's $340,000 fraud case can be closed. SPF, DKIM, and DMARC all pass, because the sender's domain really is authenticated, those checks only validate who sent it, not what it says.
Let's verify its processing through Lambda function logs in CloudWatch. The agent processes this email, it decides the "compliance update" is worth remembering and stores it in DynamoDB through the same MemoryWriter action group, with no validation in the path.
Let's now inspect Agent Memory and Response. We will ask the agent about the new item in DynamoDB and get back the fact it just stored.
Now when the compliance officer asks the agent about Account X, it repeats the fabricated clearance, "cleared," "no fraud found" , instead of surfacing the real $340,000 alert. The write came from the same Lambda role that was always allowed to write there.

Adding the defenses

Utilizing AWS Services, we can build the defenses for the attack.
Let's see them.
1) IAM role split. defenses/iam_split.tf splits the one monolithic memory-writer role into a read-only role (no PutItem, plus an explicit Deny on write actions as a backstop) and a write-only role that only the validator Lambda invoke, never the agent directly.
BEFORE
BEFORE
AFTER
AFTER
2) The validator Lambda. defenses/validator_lambda/handler.py runs every write through five gates, any one of which can reject it:
  • a regex scan against nineteen known injection patterns (ignore\s+(all\s+)?previous\s+instructions, delimiter tricks like <|system|>)
  • Bedrock Guardrails, for semantic classification
  • a DynamoDB-backed rate limiter capped at specific number of writes per session per minute
  • a n-gram similarity check against known injection fingerprints
  • an HMAC-SHA256 signature over the content and metadata, via KMS
3) TTL-based confidence decay. defenses/decay_lambda/decay_handler.py runs every 15/30 minutes and decays every memory's confidence score:
confidence = initial_confidence × e^(−decay_rate × hours_since_validation)
Verified memories start at 1.0 and decay slowly (~23 hours to the 0.3 archive threshold); unverified ones start at 0.5 and decay twice as fast (~5.5 hours). Based on different types of memories, we can decide the decay time.

Conclusion

So, now we are towards the end of the blog. That's the whole attack and its defenses.
If you're building agents with durable memory, the question to ask of your own system is the same one this lab started with: does the execution role have unconditional write access to its memory, and what's actually stopping someone from using it?
Thanks and Regards,
Sankalp Sandeep Paranjpe
https://www.linkedin.com/in/sankalp-s-paranjpe/
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article