

How to safely build LLM‑driven agents that interact with AWS without falling victim to a single‑prompt takeover.
⚠️ Disclaimer – The code below is defensive. It shows how to mitigate the AgentCorruption prompt‑injection risk, not how to exploit it. Always follow the principle of least privilege and test in a sandbox account before using in production.
<a name="step-1-prerequisites"></a>
| Item | Why you need it | Minimum version |
|---|---|---|
| AWS Account | To create IAM roles/policies and test SDK calls. | Any (use a dedicated sandbox account). |
| IAM User/Role with programmatic access | Supplies credentials for the SDK. | Must have sts:AssumeRole permission if you plan to assume a role. |
| Python 3.9+ | Core language for the Python example. | 3.9, 3.10, 3.11, 3.12 |
| Node.js 18+ (or LTS) | Runtime for the JS/TS example. | 18.x, 20.x |
Package manager – pip (Python) & npm or yarn (JS/TS) | Installs dependencies. | Latest stable. |
| Code editor (VS Code, JetBrains, etc.) | For editing and debugging. | Any. |
| Optional – LLM provider (e.g., OpenAI, Anthropic, local Llama) | The agent’s “brain”. The examples use a mock LLM call; replace with your provider’s SDK. | N/A |
Tip: Create a dedicated IAM role (
AgentCorruptionDefenderRole) that only permits the specific AWS actions you intend to expose (e.g.,s3:ListBucket). Attach a restrictive inline policy (see Common Patterns).
<a name="step-2-installation-and-setup"></a>
# 1️⃣ Create a virtual environment (recommended)
python3 -m venv .venv
source .venv/bin/activate # on Windows: .venv\Scripts\activate
# 2️⃣ Install core dependencies
pip install --upgrade pip
pip install boto3==1.34.0 # AWS SDK for Python
pip install python-dotenv==1.0.0 # load .env files
# Optional: if you plan to use a real LLM (e.g., OpenAI)
# pip install openai==1.30.0
# 1️⃣ Initialize a new Node.js project
mkdir agentcorruption-defender && cd $_
npm init -y # or yarn init -y
# 2️⃣ Install AWS SDK v3 (modular) and dotenv
npm install @aws-sdk/client-s3 @aws-sdk/credential-providers dotenv
# Optional: TypeScript support
npm install --save-dev typescript ts-node @types/node
# 3️⃣ (TS only) Create a basic tsconfig.json
npx tsc --init --rootDir src --outDir dist --esModuleInterop --resolveJsonModule --lib es2020,dom --module commonjs
<a name="step-3-basic-implementation"></a>
The following snippets illustrate a secure agent that:
list_s3_buckets).Replace the mock LLM call (
get_llm_response) with your actual provider’s SDK. The validation logic is the critical defense layer.
# file: agent.py
"""
Defensive LLM‑to‑AWS agent – mitigates AgentCorruption prompt‑injection.
"""
import os
import json
import logging
from typing import Literal
import boto3
from botocore.exceptions import BotoCoreError, ClientError
from dotenv import load_dotenv
# ----------------------------------------------------------------------
# Configuration & Logging
# ----------------------------------------------------------------------
load_dotenv() # pulls AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, etc.
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s - %(message)s",
)
logger = logging.getLogger("agentcorruption")
# ----------------------------------------------------------------------
# Constants – adjust to your least‑privilege policy
# ----------------------------------------------------------------------
ALLOWED_ACTIONS = {"list_s3_buckets"} # whitelist of safe operations
ROLE_TO_ASSUME = os.getenv(
"AGENT_ASSUME_ROLE_ARN",
"arn:aws:iam::123456789012:role/AgentCorruptionDefenderRole",
)
SESSION_NAME = "AgentCorruptionSession"
# ----------------------------------------------------------------------
# Helper: Assume a least‑privilege role via STS
# ----------------------------------------------------------------------
def assume_role(role_arn: str, session_name: str) -> dict:
"""
Returns temporary credentials dict.
Raises RuntimeError on failure.
"""
sts = boto3.client("sts")
try:
resp = sts.assume_role(
RoleArn=role_arn,
RoleSessionName=session_name,
# Optional: ExternalID for added security if your trust policy requires it
# ExternalId=os.getenv("EXTERNAL_ID"),
)
creds = resp["Credentials"]
logger.info("Assumed role %s successfully", role_arn)
return {
"aws_access_key_id": creds["AccessKeyId"],
"aws_secret_access_key": creds["SecretAccessKey"],
"aws_session_token": creds["SessionToken"],
"region_name": os.getenv("AWS_DEFAULT_REGION", "us-east-1"),
}
except (BotoCoreError, ClientError) as e:
logger.exception("Failed to assume role %s", role_arn)
raise RuntimeError(f"STS assume_role error: {e}") from e
# ----------------------------------------------------------------------
# Mock LLM – replace with real provider call
# ----------------------------------------------------------------------
def get_llm_response(user_prompt: str) -> str:
"""
In a real implementation, call your LLM (OpenAI, Anthropic, etc.)
and return the raw text.
For demo purposes we simply echo the prompt.
"""
# TODO: insert actual LLM SDK call here
return user_prompt.strip()
# ----------------------------------------------------------------------
# Core agent logic
# ----------------------------------------------------------------------
def safe_agent(user_prompt: str) -> dict:
"""
1. Validate prompt → allowed action.
2. Assume least‑privilege role.
3. Execute the action via boto3.
4. Return structured result or error.
"""
# ---- Step 1: Prompt validation -------------------------------------------------
# We only allow a very small set of deterministic commands.
# In practice you could parse the LLM output into a JSON schema.
normalized = user_prompt.lower().strip()
if normalized not in ALLOWED_ACTIONS:
msg = f"Prompt '{user_prompt}' not in allowed actions {ALLOWED_ACTIONS}"
logger.warning(msg)
return {"status": "error", "reason": "prompt_not_allowed", "message": msg}
action = normalized # e.g., "list_s3_buckets"
# ---- Step 2: Assume role -------------------------------------------------------
try:
creds = assume_role(ROLE_TO_ASSUME, SESSION_NAME)
except RuntimeError as e:
return {"status": "error", "reason": "sts_failure", "message": str(e)}
# ---- Step 3: Execute AWS call --------------------------------------------------
try:
if action == "list_s3_buckets":
s3 = boto3.client("s3", **creds)
response = s3.list_buckets()
bucket_names = [b["Name"] for b in response.get("Buckets", [])]
logger.info("Listed %d S3 buckets", len(bucket_names))
return {"status": "success", "action": action, "buckets": bucket_names}
else:
# Should never happen because of the whitelist, but keep for safety.
raise ValueError(f"Unsupported action: {action}")
except (BotoCoreError, ClientError) as e:
logger.exception("AWS call failed for action %s", action)
return {"status": "error", "reason": "aws_failure", "message": str(e)}
except Exception as e: # catch‑all for unexpected bugs
logger.exception("Unexpected error")
return {"status": "error", "reason": "unexpected", "message": str(e)}
# ----------------------------------------------------------------------
# Simple CLI for local testing
# ----------------------------------------------------------------------
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python agent.py \"<user prompt>\"")
sys.exit(1)
prompt = sys.argv[1]
result = safe_agent(prompt)
print(json.dumps(result, indent=2))
# Ensure .env contains AWS credentials for the *initial* IAM user/role
# (the one that can sts:AssumeRole the defender role)
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_DEFAULT_REGION=us-east-1
export AGENT_ASSUME_ROLE_ARN=arn:aws:iam::123456789012:role/AgentCorruptionDefenderRole
python agent.py "list_s3_buckets"
# Expected output: JSON with status: success and bucket list
python agent.py "drop all tables"
# Expected output: JSON with status: error, reason: prompt_not_allowed
// file: src/agent.ts
/**
* Defensive LLM‑to‑AWS agent (Node.js) – mitigates AgentCorruption.
* Uses AWS SDK v3 (modular) and dotenv for config.
*/
import { config } from "dotenv";
import { STSClient, AssumeRoleCommand, AssumeRoleCommandOutput } from "@aws-sdk/client-sts";
import { S3Client, ListBucketsCommand, ListBucketsCommandOutput } from "@aws-sdk/client-s3";
import { Logger } from "./logger"; // simple wrapper (see below)
config(); // loads .env into process.env
// ----------------------------------------------------------------------
// Logger (tiny wrapper around console)
// ----------------------------------------------------------------------
export class Logger {
static info(msg: string) {
console.info(`[INFO] ${new Date().toISOString()} - ${msg}`);
}
static warn(msg: string) {
console.warn(`[WARN] ${new Date().toISOString()} - ${msg}`);
}
static error(msg: string, err?: unknown) {
console.error(`[ERROR] ${new Date().toISOString()} - ${msg}`, err);
}
}
// ----------------------------------------------------------------------
// Constants – adjust to your least‑privilege policy
// ----------------------------------------------------------------------
const ALLOWED_ACTIONS = new Set(["list_s3_buckets"]);
const ROLE_TO_ASSUME =
process.env.AGENT_ASSUME_ROLE_ARN ||
"arn:aws:iam::123456789012:role/AgentCorruptionDefenderRole";
const SESSION_NAME = "AgentCorruptionSession";
// ----------------------------------------------------------------------
// Helper: Assume role via STS
// ----------------------------------------------------------------------
async function assumeRole(roleArn: string): Promise<AssumeRoleCommandOutput> {
const sts = new STSClient({});
const params = {
RoleArn: roleArn,
RoleSessionName: SESSION_NAME,
// ExternalId: process.env.EXTERNAL_ID, // if needed
};
try {
const data = await sts.send(new AssumeRoleCommand(params));
Logger.info(`Assumed role ${roleArn}`);
return data;
} catch (err) {
Logger.error(`Failed to assume role ${roleArn}`, err);
throw new Error(`STS assumeRole failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
// ----------------------------------------------------------------------
// Mock LLM – replace with real provider call
// ----------------------------------------------------------------------
function mockLLM(prompt: string): string {
// In production, call your LLM SDK here and return the raw text.
return prompt.trim();
}
// ----------------------------------------------------------------------
// Core agent logic
// ----------------------------------------------------------------------
export async function safeAgent(userPrompt: string): Promise<any> {
// 1️⃣ Validate prompt
const normalized = userPrompt.toLowerCase().trim();
if (!ALLOWED_ACTIONS.has(normalized)) {
const msg = `Prompt "${userPrompt}" not in allowed actions ${Array.from(
ALLOWED_ACTIONS
).join(", ")}`;
Logger.warn(msg);
return { status: "error", reason: "prompt_not_allowed", message: msg };
}
const action = normalized as "list_s3_buckets";
// 2️⃣ Assume least‑privilege role
let creds: any;
try {
const assumeResp = await assumeRole(ROLE_TO_ASSUME);
creds = {
accessKeyId: assumeResp.Credentials?.AccessKeyId,
secretAccessKey: assumeResp.Credentials?.SecretAccessKey,
sessionToken: assumeResp.Credentials?.SessionToken,
region: process.env.AWS_DEFAULT_REGION ?? "us-east-1",
};
} catch (err) {
return { status: "error", reason: "sts_failure", message: String(err) };
}
// 3️⃣ Execute AWS call
try {
if (action === "list_s3_buckets") {
const s3 = new S3Client(creds);
const data = await s3.send(new ListBucketsCommand({}));
const bucketNames = data.Buckets?.map((b) => b.Name ?? "") ?? [];
Logger.info(`Listed ${bucketNames.length} S3 buckets`);
return { status: "success", action, buckets: bucketNames };
} else {
// Should never happen because of the whitelist
throw new Error(`Unsupported action: ${action}`);
}
} catch (err) {
Logger.error(`AWS call failed for action ${action}`, err);
return { status: "error", reason: "aws_failure", message: String(err) };
}
}
// ----------------------------------------------------------------------
// Simple CLI for local testing (ts-node)
// ----------------------------------------------------------------------
if (require.main === module) {
const arg = process.argv[2];
if (!arg) {
console.error('Usage: ts-node src/agent.ts "<user prompt>"');
process.exit(1);
}
safeAgent(arg)
.then((res) => console.log(JSON.stringify(res, null, 2)))
.catch((e) => {
console.error("Unexpected error:", e);
process.exit(1);
});
}
# 1️⃣ Create .env (never commit this file)
cat > .env <<EOF
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
AWS_DEFAULT_REGION=us-east-1
AGENT_ASSUME_ROLE_ARN=arn:aws:iam::123456789012:role/AgentCorruptionDefenderRole
EOF
# 2️⃣ Install dependencies (if not done already)
npm install
# 3️⃣ Run with ts-node (or compile first)
npx ts-node src/agent.ts "list_s3_buckets"
# Expected: JSON with status: success and bucket list
npx ts-node src/agent.ts "delete everything"
# Expected: JSON with status: error, reason: prompt_not_allowed
<a name="step-4-configuration"></a>
| Variable | Description | Example | Required? |
|---|---|---|---|
AWS_ACCESS_KEY_ID | Access key of the initial IAM user/role that can sts:AssumeRole the defender role. | AKIAIOSFODNN7EXAMPLE | ✅ |
AWS_SECRET_ACCESS_KEY | Secret key matching the above. | wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY | ✅ |
AWS_DEFAULT_REGION | Default AWS region for SDK calls. | us-east-1 | ✅ |
AGENT_ASSUME_ROLE_ARN | ARN of the least‑privilege role the agent will assume. | arn:aws:iam::123456789012:role/AgentCorruptionDefenderRole | ✅ |
EXTERNAL_ID (optional) | If the role’s trust policy requires an external ID, set it here. | my-random-id-123 | ❌ |
LOG_LEVEL (optional) | Control verbosity (debug, info, warn, error). | info | ❌ |
IAM role example (least‑privilege) – attach this inline policy to AgentCorruptionDefenderRole:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["sts:AssumeRole"],
"Resource": "arn:aws:iam::123456789012:role/AgentCorruptionDefenderRole"
},
{
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetObject"],
"Resource": [
"arn:aws:s3:::my-public-bucket",
"arn:aws:s3:::my-public-bucket/*"
]
}
]
}
Note: The trust policy of the role should allow
sts:AssumeRolefrom the initial IAM user/role (or from an AWS service like EC2/Lambda if you run the agent there). Add anexternal_idcondition if you want extra protection.
<a name="step-5-common-patterns"></a>
| Pattern | Why it helps against AgentCorruption | Code snippet |
|---|---|---|
| Prompt whitelisting / schema validation | Guarantees the LLM can only request pre‑approved actions. | See ALLOWED_ACTIONS and the if (!ALLOWED_ACTIONS.has(normalized)) check. |
| Assume a dedicated, restricted role | Even if the LLM is compromised, the temporary credentials have limited scope. | assumeRole() → STS → temporary creds. |
| Short-lived session tokens | Limits the window of abuse if credentials leak. | STS AssumeRole returns credentials valid for 15 min–12 h (configure via DurationSeconds). |
| Input sanitization & output encoding | Prevents injection of CLI metacharacters or SQL/Lambda payloads. | We lower‑case and trim the prompt; never concatenate raw user text into CLI commands. |
| Audit logging (CloudTrail + custom logs) | Provides visibility: every assume‑role and S3 call is recorded. | Enable CloudTrail on the account; our Logger writes to stdout (can be forwarded to CloudWatch Logs). |
| Network isolation (VPC endpoints, SCPs) | Blocks accidental data exfiltration even if credentials are misused. | Deploy the agent inside a VPC with an S3 VPC endpoint and deny s3:* to * via SCP except the allowed bucket. |
| Rate limiting / throttling | Stops an attacker from rapidly enumerating resources. | Use AWS SDK built‑in retry mode or API Gateway throttling if exposing via HTTP. |
| Separation of concerns | The LLM only decides what to do; a thin validation layer decides if it’s allowed. | safeAgent() splits validation, role assumption, and execution. |
<a name="step-6-troubleshooting"></a>
| Symptom | Likely cause | Fix |
|---|---|---|
InvalidClientTokenId: The security token included in the request is invalid | Expired or incorrect initial credentials. | Verify AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY are correct and not expired. Re‑issue if needed. |
AccessDenied when assuming role | The initial principal lacks sts:AssumeRole on the target role, or trust policy misconfigured. | Add sts:AssumeRole permission to the user/role; ensure the role’s trust policy includes the principal’s ARN. |
AccessDenied on S3 ListBucket | The assumed role’s policy does not grant s3:ListBucket on the target bucket. | Attach the least‑privilege policy shown in Step 4 to the role. |
Prompt not allowed error even though you typed list_s3_buckets | Case‑sensitivity or extra whitespace. | The agent lower‑cases and trims; ensure you send exactly list_s3_buckets (any extra text will be rejected). |
npm ERR! code ERESOLVE when installing | Version conflict between AWS SDK packages. | Delete node_modules and package-lock.json, then run npm install again. |
Module not found: './logger' (TS) | Logger file missing. | Create a simple logger.ts (see snippet in Step 3) or replace Logger calls with console.*. |
Unexpected token 'export' when running node directly | Trying to run TS file without compiling. | Use ts-node or compile: tsc && node dist/agent.js. |
AWS SDK v3: MissingRegion | Region not set in config or env. | Ensure AWS_DEFAULT_REGION is set or pass region: explicitly when constructing the client. |
<a name="step-7-production-checklist"></a>
| ✅ Item | Description |
|---|---|
| Least‑privilege IAM role | Role attached to the agent only permits the exact AWS actions you need (e.g., s3:ListBucket on specific buckets). |
| External ID (if applicable) | Add a random secret to the role’s trust policy and provide it via EXTERNAL_ID. |
| Short session duration | Set DurationSeconds on AssumeRoleCommand to the minimum needed (e.g., 900 seconds). |
| Secret management | Store AWS credentials for the initial principal in a secrets manager (AWS Secrets Manager, HashiCorp Vault, or CI/CD protected variables). Never commit .env. |
| Logging & monitoring | Enable CloudTrail, send logs to CloudWatch Logs or a SIEM, and create alerts for AssumeRole and ListBuckets outside expected patterns. |
| Network controls | Run the agent inside a VPC with S3 VPC endpoint; restrict outbound traffic to AWS service endpoints only. |
| Input validation | Enforce a strict allow‑list of actions; consider using a JSON schema (e.g., ajv) if you expand beyond simple strings. |
| Output sanitization | Never return raw AWS responses to the end‑user; map to a safe DTO (e.g., just bucket names). |
| Rate limiting | If exposing via an API gateway, enable throttling (e.g., 5 req/s) and use AWS SDK built‑in retry mode with exponential back‑off. |
| Dependency hygiene | Lock versions (package-lock.json/requirements.txt), run npm audit / pip safety regularly, and keep SDKs up‑to‑date. |
| Testing in a sandbox | Validate the flow in a separate AWS account with no production data before promoting to prod. |
| Incident response plan | Know how to revoke the assumed role’s session (sts:RevokeSession) and rotate the initial credentials quickly. |
Defend against AgentCorruption by never trusting the LLM’s output directly.
Validate the prompt, assume a tightly‑scoped role, and execute only the vetted AWS call. The code snippets above give you a ready‑to‑run, production‑grade foundation in both Python and TypeScript that you can extend (more actions, richer schemas, HTTP wrappers, etc.) while keeping the attack surface minimal.
Happy and safe coding! 🚀
Source: Dark Reading
Follow ICARAX for more AI insights and tutorials.
