Back to Cheat Sheets

📊 Inadequate Function Monitoring and Logging

OWASP Serverless Top 10 - SAS-5

MEDIUM RISK

📋 What Is It?

Inadequate Function Monitoring and Logging occurs when a serverless application cannot see what its own functions are doing well enough to notice an attack. Functions are ephemeral (handle one event and vanish), event-driven, and highly distributed, so injection attempts, credential abuse, reconnaissance, exfiltration, and cost abuse spread across many short-lived invocations without ever surfacing an alert. Default logs are operational, not security-oriented - they answer "did it run and how long," not "who did this, with what identity, and was it malicious." This is rarely the vulnerability an attacker exploits; it is the reason every other exploitation succeeds quietly.

SAS-5 OWASP Rank
Dwell Time Cost
Blind By Default

⚠️ Common Exploits

  • Default Logs Only: function prints START/END, nothing security-relevant
  • No Correlation: each function logs to its own stream; nothing ties them together
  • No Tracing: a request fanning out across functions cannot be followed
  • No Anomaly Alerting: a 100x invocation spike noticed on the invoice
  • Error-Only Alerts: authorised-but-abusive reads all return 200
  • Short/Mutable Retention: function role can shorten retention or delete streams
  • Managed-Service Blind Spots: S3/DynamoDB/IAM data-plane events uncaptured

🔴 Attack Flow

1. Enter quietly through an event source no one is watching
↓
2. Operate in blind spots: act across many short-lived invocations
↓
3. Avoid the only alarm that exists: produce "successful" 200 responses
↓
4. Persist, exfiltrate, and spend for weeks - no alert fires
↓
5. IMPACT: Breach discovered by a bill or a third party!

❌ Vulnerable Code

// Bad: nothing security-relevant is logged; a silent denial leaves no trace exports.handler = async (event) => { const id = event.pathParameters.id; const tenant = event.requestContext.authorizer.tenantId; const order = await getOrder(id); if (order.tenantId !== tenant) { return { statusCode: 403, body: 'Forbidden' }; ← no log, no identity, no id } return { statusCode: 200, body: JSON.stringify(order) }; }; // CloudWatch shows only START/END/REPORT. A tenant-walking sweep is invisible. # Bad: dumps the ENTIRE event - leaks tokens, PII, secrets into logs def handler(event, context): print("event:", json.dumps(event)) ← secrets in logs, still no security event data = read_records(authenticate(event)) return {"statusCode": 200, "body": json.dumps(data)} # Bad: tracing disabled (template.yaml) - each invocation is an island Resources: OrderApi: Type: AWS::Serverless::Function Properties: Handler: index.handler # no Tracing: Active → the request path across functions is invisible

✅ Secure Code

// Good: structured security events with identity + correlation ids function securityEvent(event, context, fields) { console.log(JSON.stringify({ ts: new Date().toISOString(), level: 'SECURITY', request_id: context.awsRequestId, // correlate this invocation trace_id: process.env._X_AMZN_TRACE_ID, // correlate across the chain function: context.functionName, source_ip: event.requestContext?.identity?.sourceIp, identity: event.requestContext?.authorizer?.principalId, ...fields })); } exports.handler = async (event, context) => { if (order.tenantId !== tenant) { securityEvent(event, context, { event: 'authz_denied', resource: `orders/${id}`, outcome: 'DENY', reason: 'cross_tenant_access' }); return { statusCode: 403, body: 'Forbidden' }; } }; // A sweep now emits correlated authz_denied events from one identity → alertable. # Good: log the decision + a volume signal, never the raw secret security_event(context, event="sensitive_read", identity=user.id, outcome="ALLOW", record_count=len(records)) # surfaces bulk reads # Good: enable distributed tracing + a cost/invocation alarm (SAM) Globals: Function: Tracing: Active # X-Ray spans the whole chain Resources: InvocationSpikeAlarm: Type: AWS::CloudWatch::Alarm Properties: MetricName: Invocations # also alarm on EstimatedCharges Threshold: 1000 ComparisonOperator: GreaterThanThreshold

✓ Prevention Checklist

  • Emit structured security events from function code
  • Include identity, source IP, resource, and outcome
  • Carry a shared request/trace id to correlate across the chain
  • Enable distributed tracing (X-Ray / OpenTelemetry)
  • Centralize logs; do not leave them siloed per function
  • Alert on error, invocation, and cost spikes - not just crashes
  • Alert on unusual IAM/role use (services never normally called)
  • Log record volume to surface bulk reads / exfiltration
  • Redact secrets; never dump the whole event
  • Capture CloudTrail management + data-plane events
  • Ship logs off-account; deny the workload role log-delete
  • Baseline normal behaviour and wire alerts into an IR process

🔍 Detection & Tools

Logging & Tracing:

CloudWatch Logs AWS X-Ray OpenTelemetry CloudTrail Powertools for Lambda

Alerting & Analysis:

CloudWatch Alarms AWS Budgets GuardDuty Security Hub SIEM / OpenSearch

How to Test:

  • Trigger an authz denial - does a structured event appear?
  • Follow one request across functions via a trace id
  • Drive an invocation spike - does an alarm fire?
  • Call a service the function never uses - is it flagged?
  • Confirm secrets are redacted, not dumped, in logs
  • Check retention length and whether the role can delete logs

🌍 Real-World

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

  • Long-dwell cloud intrusions: attackers operate inside an account for weeks or months; execution logs exist but there is no security-event logging, correlation, or anomaly alerting, so intrusions are found late - often by an outside party.
  • Denial-of-wallet against public functions: an internet-reachable function with no rate limit and no alert on invocation count or estimated cost; pay-per-use billing scales silently until the invoice arrives.
  • Managed-service blind spots: access or exfiltration happening between services (an S3 object copied, an IAM policy attached) leaves no trace because CloudTrail data events were never captured.
  • Evidence destruction: a compromised function whose role can shorten retention or delete log streams erases the operational trail after acting.
  • Successful-but-abusive access: a tenant-walking sweep returns only 200s, so an error-only alerting strategy sees nothing.

📌 Quick Tips

  • DO NOT rely on default execution logs for detection
  • DO NOT dump whole events (secrets land in logs)
  • DO NOT let the workload role manage its own log store
  • DO log security events with identity and outcome
  • DO correlate and trace across the whole chain
  • DO alert on invocation and cost spikes
  • DO instrument before the incident, not after

📜 Compliance

Related Standards:

  • OWASP Serverless Top 10 - SAS-5
  • CWE-778 Insufficient Logging
  • CWE-223 Omission of Security-relevant Information
  • PCI-DSS Requirement 10 - Logging & Monitoring
  • SOC 2 CC7.2 / ISO 27001 A.12.4