Back to Cheat Sheets

🔀 Serverless Business Logic Manipulation

OWASP Serverless Top 10 - SAS-9

HIGH RISK

📋 What Is It?

Serverless Business Logic Manipulation is the abuse of an application's intended flow. A serverless app is rarely one program-it is many small functions chained by events, queues, topics, and orchestration (Step Functions, SQS/SNS, EventBridge). The complete business rule ("validate the cart, charge the card, then fulfil") is distributed across independent functions, and each function tends to assume whatever ran before it ran correctly. That assumption is the vulnerability: an attacker who reaches a later step directly-or reorders, replays, or forges the events connecting the steps-makes the app do what the designed sequence never would: fulfil an unpaid order, redeem a credit twice, or skip a fraud check. The requests are usually well-formed; the flaw is that sequence and preconditions are not enforced at every step.

SAS-9 OWASP Rank
At-Least-Once Delivery Default
Low-Visibility Each Call Looks Valid

⚠️ Common Exploits

  • Direct Invoke of a Later Step: Call fulfilOrder directly, skipping validation/payment/authz
  • Forged Event: Publish order.paid to a topic the consumer trusts
  • Replay: Re-submit a genuine refund.approved event N times
  • Duplicate Double-Spend: At-least-once delivery runs a non-idempotent handler twice
  • State Tampering: Write paid=true to DynamoDB via a loose IAM path
  • Check-Then-Act Race: Concurrent runs pass a single-use check before any write lands
  • Out-of-Band Guardrail Bypass: Reach a guarded action through a path that misses the check

🔴 Attack Flow

1. Map the workflow
(which functions/events/queues/state form the chain?)
↓
2. Find the trusting step
(which later step acts WITHOUT re-checking?)
↓
3. Find the door
(invoke it? publish its event? write its state? replay?)
↓
4. Enter the flow late - or twice
(trigger out of order, or deliver again)
↓
5. IMPACT: Goods shipped, access granted, wallet credited - free!

❌ Vulnerable Code

// fulfilOrder is invoked as a later step. It ASSUMES payment already ran. exports.handler = async (event) => { const { orderId, items } = event; // No re-check: if this function is reached at all, it ships. // Direct invoke, forged event, or replay all bypass validation + payment. await ship(orderId, items); return { ok: true }; }; # SQS/SNS are AT-LEAST-ONCE. This credits the wallet on EVERY delivery. exports.handler = async (event) => { for (const rec of event.Records) { const { userId, amount } = JSON.parse(rec.body); await creditWallet(userId, amount); ← duplicate delivery = double credit } }; # serverless.yml - over-exposed and over-privileged internal step functions: fulfilOrder: handler: src/fulfil.handler url: true ← PUBLIC Function URL: anyone can invoke it provider: iam: { role: { statements: [ { Action: "*", Resource: "*" } ] } } ← foothold = takeover

✅ Secure Code

// fulfilOrder re-establishes payment/authz/state itself - it trusts nothing. exports.handler = async (event) => { const orderId = requireString(event.orderId); const order = await getOrder(orderId); // 1. authoritative state if (!order) throw new Error('Unknown order'); if (order.status !== 'PAID') throw new Error('Not paid'); // 2. re-verify if (order.fraudReview !== 'PASS') throw new Error('Fraud incomplete'); if (order.fulfilled === true) return { ok: true }; // 3. idempotent await assertAuthorized(event.principal, 'order:fulfil', orderId); await ship(orderId, order.items); // items from trusted state, not event await markFulfilled(orderId); // atomic compare-and-set }; # Idempotency: conditional write makes the effect exactly-once (Python) def claim(idem_key): try: ddb.put_item(TableName="processed_events", Item={"k": {"S": idem_key}}, ConditionExpression="attribute_not_exists(k)") # insert only if unseen return True except ClientError as e: if e.response["Error"]["Code"] == "ConditionalCheckFailedException": return False # duplicate -> no-op raise # idem_key = f"credit:{userId}:{rec.messageId}" - bound to message identity # serverless.yml - no public URL; ONLY Step Functions may invoke; scoped role resources: { Resources: { FulfilPermission: { Type: AWS::Lambda::Permission, Properties: { Action: lambda:InvokeFunction, Principal: states.amazonaws.com } } } }

✓ Prevention Checklist

  • Re-validate authz + required prior state at every step
  • The step performing the effect verifies the right to do it
  • Confirm payment by re-reading authoritative state / the PSP
  • Make every side-effect handler idempotent
  • Derive idempotency keys from event identity, not client input
  • Use atomic conditional writes (compare-and-set)
  • Orchestrate server-side (Step Functions); don't let the client drive order
  • Sign inter-step state across trust boundaries; validate it
  • No public URL / broad invoke on internal steps
  • Verify each event's source ARN and signature
  • Assume at-least-once; use FIFO/dedup/DLQ where it helps
  • Reconcile cross-step invariants continuously

🔍 Detection & Tools

Controls & Services:

AWS Step Functions DynamoDB Conditional Writes SQS FIFO / Dedup Dead-Letter Queues SNS Signature Verification Lambda Powertools Idempotency

Monitoring & Review:

Invariant Reconciliation CloudTrail (invoke source) Abuse-Case Testing Design Review

How to Test:

  • Invoke the last step directly - does it refuse?
  • Replay/duplicate a message - does the effect run twice?
  • Publish a forged event to a consumed topic/queue
  • Tamper a paid/approved flag and re-trigger downstream
  • Fire concurrent single-use redemptions (race)
  • Reconcile fulfilments against captured payments

🌍 Real-World

Documented as a class of problem across event-driven and serverless systems-these are recurring incident patterns, not specific named breaches:

  • Workflow-Bypass Fulfilment: A fulfilment function is reachable (broad invoke, Function URL, or a topic it consumes) and trusts that payment already succeeded; an attacker triggers fulfilment for orders never paid or risk-checked. Root cause: the precondition was enforced only by the step's position in the intended flow.
  • Duplicate-Processing Double-Spend: A wallet-credit/refund/coupon handler performs its financial effect without deduplicating; normal at-least-once delivery-or deliberate re-submission-applies it multiple times.
  • Trusted-State Tampering: A downstream step reads a status/approved/paid flag; a separate over-permissioned path sets it independently, so the step behaves as though earlier controls passed.

📌 Quick Tips

  • DO NOT trust that an upstream step ran
  • DO NOT assume exactly-once delivery
  • DO NOT expose internal steps with public URLs
  • DO re-validate authz + state at every step
  • DO make side-effect handlers idempotent
  • DO verify event source and integrity
  • DO reconcile cross-step invariants

📜 Compliance

Related Standards:

  • OWASP API Top 10 - Business Logic / BOLA
  • PCI-DSS Requirement 6.2 - Secure Development
  • NIST 800-53 SC-7 / SI-10 - Flow & Input Integrity
  • AWS Well-Architected - Security & Reliability
  • SOC 2 CC8.1 - Change & Process Integrity