How to prevent public AWS Lambda abuse using API Gateway

·

5 min read

How to prevent public AWS Lambda abuse using API Gateway

The Risks of Public AWS Lambda Functions

Public AWS Lambda functions are powerful, but without proper safeguards, they can be misused. Here’s what can go wrong:

  1. Open Access: If anyone can trigger your Lambda function, it might be used for the wrong reasons, leading to overuse or sensitive data exposure.

  2. Cost Issues: Lambda charges are based on usage. If someone repeatedly triggers your function, your bills could skyrocket.

  3. Data Theft: A Lambda function dealing with sensitive data can be a target for data leaks if not secured properly.

  4. Service Disruption: Excessive traffic, whether intentional or not, can overload your Lambda functions, disrupting the service.

In this guide I'll focus on #2.

The steps

Here're the steps in order we'll do.

  1. Create a Lambda function + test

  2. Add an API gateway endpoint to it

  3. Create an API key with limited usage on it + test

The Lambda function

First we need to create an example Lambda function:

Let's fill in the lambda function with code that adds a unix timestamp to the request JSON:

import json
import time

def lambda_handler(event, context):
    # Parsing the JSON body from the event
    data = json.loads(event['body'])

    # Append the current Unix timestamp
    data['timestamp'] = int(time.time())

    # Return the modified data as JSON
    return {
        'statusCode': 200,
        'body': json.dumps(data)
    }

Click Deploy.

Once done, you can add a new test event and test with this example that imitates an API gateway JSON:

{
    "resource": "/formsubmit",
    "path": "/formsubmit",
    "httpMethod": "POST",
    "headers": {
        "Content-Type": "application/json",
        "Accept": "application/json"
    },
    "queryStringParameters": null,
    "multiValueQueryStringParameters": null,
    "pathParameters": null,
    "stageVariables": null,
    "requestContext": {
        "requestTime": "30/Jan/2024:12:31:45 +0000",
        "path": "/prod/formsubmit",
        "protocol": "HTTP/1.1",
        "stage": "prod",
        "domainName": "api.example.com",
        "requestId": "123456789",
        "requestTimeEpoch": 1580389905401,
        "accountId": "123456789012",
        "apiId": "abcdefghij"
    },
    "body": "{\"name\": \"John Doe\", \"email\": \"johndoe@example.com\"}",
    "isBase64Encoded": false
}

In the execution results you should see something like this, having the timestamp added:

Response
{
  "statusCode": 200,
  "body": "{\"name\": \"John Doe\", \"email\": \"johndoe@example.com\", \"timestamp\": 1706616281}"
}

API Gateway

At this point you have a lambda function that can be used within AWS.

In order to call it from outside your architecture, you can have an API Gateway endpoint triggering your Lambda function.

Something like this:

API gateway will give us the power to restrict the incoming calls using API keys.

So as a next step...create a trigger in your lambda function and select API gateway.

Create a REST Api:

Then visit your newly created API.

Deploy this to any stage, I used default, then take a note of the invoke URL that's something similar to this, and append your function name to it:

123456asdfg.execute-api.us-east-1.amazonaws..

You can use this URL to test your endpoint.

Here's the fun part, let's try it!

You can curl the endpoint from any terminal:

curl -X POST "https://12345asdfg.execute-api.us-east-1.amazonaws.com/default/timestamper" \
     -H "Content-Type: application/json" \
     -d "{\"name\": \"John Doe\", \"email\": \"johndoe@example.com\"}"
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   129  100    77  100    52    180    122 --:--:-- --:--:-- --:--:--   304{"name": "John Doe", "email": "johndoe@example.com", "timestamp": 1706626702}

As you see the response has come back and now our gateway endpoint is public 😱

What we need to do now is to limit the access of it via API key.

Gateway API keys

Within API Gateway => APIs => API keys => Create API key section, create an API key. You only need to add a name to it and autogenerate it.

Similarly, just above that, you can add a API Gateway => APIs => Usage plan => Create usage plan

And this is where the magic happens.

Right here you can restrict the API in various ways, as an example:

This will provide

  • 10 calls a day with this API key

  • max. 1 call a second

  • max. 1 call at a time

Of course it is fairly restrictive, but this will prevent anyone calling your 100s of times a second racking up a nice Lambda and API Gateway bill for you.

Once done, go back to your API key and add it to the usage plan. You can find this option under the "Actions" button.

You can now go back to your API and modify the resource with the EDIT button:

Once clicked, you can turn on "Api key required", then save.

Before you re-try your curl, make sure you "Deploy API" and wait a few minutes.

After that, your response should be something like this:

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    75  100    23  100    52     59    133 --:--:-- --:--:-- --:--:--   193{"message":"Forbidden"}

Great! Now our endpoint requires an API key.

That's great, but how do I send the API key? 🤔

Well, here's how:

curl -X POST "https://12345asdfg.execute-api.us-east-1.amazonaws.com/default/timestamper2" \
      -H "Content-Type: application/json" \
      -H "x-api-key: 1234567890asdfghjkl" \
      -d "{\"name\": \"John Doe\", \"email\": \"johndoe@example.com\"}"
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   129  100    77  100    52    138     93 --:--:-- --:--:-- --:--:--   232{"name": "John Doe", "email": "johndoe@example.com", "timestamp": 1706632463}

If we send request too quickly we'll get this response:

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    83  100    31  100    52     71    120 --:--:-- --:--:-- --:--:--   193{"message":"Too Many Requests"}

And if we run out of the daily limit we'll get this:

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    80  100    28  100    52     81    151 --:--:-- --:--:-- --:--:--   234{"message":"Limit Exceeded"}

Closing thoughts

There's of course plenty of other ways to secure your lambda function, to name a few:

  • AWS WAF for an extra security layer. It’s like having a guard to block unwanted traffic. With this, you can selectively deny traffic.

  • Control Access with IAM who can use your Lambda functions.

  • Use CloudWatch for detailed logging and alerts.

  • Set limits on how many instances of your Lambda function can run at once via Lambda concurrency. This prevents your system from getting overwhelmed by too much traffic.