
Serverless changes what you attack. There is no server to port-scan, no OS to exploit, no long-lived host to pivot through. A function spins up, runs your code with an IAM role, and disappears. That collapses the attack surface down to four things: the function code, the events that trigger it, the permissions it carries, and the dependencies it ships with. Get those right and serverless is genuinely hard to break; get them wrong and a single event injection inherits an over-privileged role straight into your account.
This guide covers serverless penetration testing for AWS Lambda, Azure Functions, and Google Cloud Functions, organized by that four-part surface rather than a traditional kill chain. You will see a real non-HTTP injection payload, the credentials a function leaks once you have code execution, a dependency-scan finding, and a sample findings table. The OWASP Serverless Top 10 anchors the framework, and written authorization is assumed.
Serverless penetration testing assesses functions-as-a-service applications (AWS Lambda, Azure Functions, Google Cloud Functions) by focusing on code, event sources, IAM permissions, and dependencies rather than infrastructure. The provider owns the runtime, patching, and isolation, so the testable surface shifts almost entirely into the application and its configuration.
The mental model is different from a monolith. A monolith has a handful of front doors; a serverless app has dozens of functions, each triggered by different event sources, each with its own role. The attack surface is wide and shallow. The first move is to inventory it, because the application's serverless.yml, SAM template, or Terraform is gold: it lists every function, its triggers, and its role in one place. The OWASP Serverless Top 10 reframes the familiar web risks for this model, and much of it overlaps with the API and application work in our web application pentesting checklist.
Event injection is the serverless equivalent of injection attacks, and it is broader because functions accept input from many event sources, not just HTTP. A Lambda can fire from API Gateway, an SQS or SNS message, an S3 object-created event, a DynamoDB stream, an SES email, or an IoT message. Each carries attacker-influenceable data, and developers trust event data from those internal-feeling sources they would never trust from a web form.
The bug pattern is classic injection: untrusted event data reaches a dangerous sink (a database query, an OS command, a dynamic eval, a downstream API call) without validation. The trick is delivering the payload through a non-HTTP trigger. For an S3-triggered function that builds a query from the object key, you write a malicious key and let the object-created event carry it in:
$ aws s3api put-object --bucket uploads-prod \
--key "orders'; DROP TABLE orders;--.jpg" --body ./benign.txt
{"ETag": "\"9b2cf...\""}
# Function's CloudWatch log, triggered by the upload:
INFO processing key: orders'; DROP TABLE orders;--.jpg
ERROR near "orders": syntax error <- the key reached the SQL sink unvalidatedThat error line is the proof: the object key flowed straight into a query. Because non-HTTP triggers feel internal, validation is often skipped entirely, so test every event source, not just the API. The underlying injection logic carries over from our SSRF testing guide and standard injection testing.
Over-privileged execution roles are the highest-impact serverless finding because a compromised function inherits whatever its role can do. The moment you get code execution through an event injection or a vulnerable dependency, you operate with the function's IAM permissions. If that role carries wildcard actions or broad data access, a single function bug becomes account-wide compromise. The fix is one role per function, scoped to exactly what it needs. During testing, read each function's role then model its reach:
$ aws lambda get-function-configuration --function-name order-processor --query 'Role' --output text
arn:aws:iam::111122223333:role/order-processor-role
$ aws iam list-attached-role-policies --role-name order-processor-role \
--query 'AttachedPolicies[].PolicyName' --output text
AmazonS3FullAccess SecretsManagerReadWrite <- way too broad for a thumbnail job
$ pmapper query "who can do secretsmanager:GetSecretValue with *"
order-processor-role can do secretsmanager:GetSecretValue with *Those two attached managed policies are the whole problem: a thumbnail function should never read every secret in the account. In Azure and GCP, check the managed identity or service-account bindings the same way. Functions also expose their environment variables to any code that runs in them, so any secret stored there is readable post-compromise. The IAM escalation paths in our AWS penetration testing guide apply directly to these roles.
Third-party dependencies are a larger risk in serverless than in traditional apps because each function ships with its full dependency tree and runs with the function's permissions. A vulnerable npm or PyPI package pulled into a function can give code execution that immediately holds the function's IAM role. Scan the actual deployment package, not just the repo, since build steps introduce drift:
$ aws lambda get-function --function-name order-processor --query 'Code.Location' --output text
https://prod-lambda.s3.amazonaws.com/...zip
$ unzip -o code.zip -d pkg && pip-audit -r pkg/requirements.txt
Name Version ID Fix Versions
------- -------- ------------------- ------------
Pillow 9.0.0 GHSA-8vj2-vxx3-667w 9.0.1 <- known RCE via crafted image, on the upload pathThat Pillow line is exploitable precisely because the function processes uploaded images. Secrets are the second issue. Once you have code execution inside a Lambda, environment variables and the runtime API hand you the role credentials directly, the same primitive a VM SSRF would reach:
$ env | grep AWS_
AWS_ACCESS_KEY_ID=ASIA... <- the execution role's temporary creds
AWS_SECRET_ACCESS_KEY=...
AWS_SESSION_TOKEN=...
AWS_LAMBDA_RUNTIME_API=127.0.0.1:9001Export those and you are the function's role from your own machine. Prefer a secrets manager (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager) with the function granted least-privilege read access at runtime, and never put long-lived secrets in environment variables.
On a recent assessment of a retailer's image pipeline, the critical I reported started in a thumbnail function triggered by S3 uploads. The image library had a known command-injection bug, the function role held s3:GetObject on every bucket plus secretsmanager:GetSecretValue *, and one uploaded file gave code execution that read the production database secret. No posture tool flagged it because every individual setting looked normal. The findings table below is that report excerpt.
Every fix is config. Scope the role to one function's actual needs, validate every event source including non-HTTP triggers, patch the dependency, and move the secret out of reach. The role JSON below is what a thumbnail function should actually carry, instead of two wildcard managed policies:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::uploads-prod/thumbnails/*"
}]
}A practical edge case that catches teams out: cold-start reuse. Lambda keeps an execution environment warm between invocations, so secrets or tokens written to /tmp or process memory by one request can be read by a later one if the function does not clean up. Test for state that survives across invocations, because it turns a one-shot injection into persistent access for the life of the warm container. Posture scanners miss this entire class because the danger lives in code paths, event chains, and runtime state, not static config. A scanner flags a role with s3:* but cannot tell whether an injection reaches a sink, whether a vulnerable dependency is on an executed path, or whether a non-HTTP trigger is validated. So validate the chains by hand, anchor the methodology to the OWASP Serverless Top 10, use pmapper to model role blast radius, run SCA on the deployment package, and use Prowler or ScoutSuite for misconfigured functions and overly permissive resource policies. Because serverless apps deploy constantly and each deploy can widen a role, this is a natural fit for continuous, agentic pentesting rather than a yearly pass. Fold the config-level findings into your cloud security posture checklist.