Harden your Serverless API (The Basics)
Serverless Functions (SFs) changed the API game. Developers can build and deploy an API without any knowledge of infrastructure controls. No checks by a DevOps team, a security team, or other Enterprise IT groups that traditionally could control the launch of new business applications.
Absolute, total freedom!
Plus, gone are the days in which APIs were limited to HTTP events. Any cloud resource is now a potential API trigger that adds highly beneficial flexibility to design cloud-native applications.
However, with great freedom comes great danger…
This ease of deployment and a variety of input sources create numerous, complex attack vectors that are difficult to grasp. In other words, unintended attack vectors can easily pop up in this application deployment model, and instant deployment of an application can mean instant exposure to potential risks such as unintended data breaches and unauthorized data exfiltration.
For starters, they can consume data from a variety of sources such as HTTP endpoints, message queues, cloud storage, IoT devices, etc. As a consequence, the size of the attack surface is directly proportional to this flexibility.
Serverless architectures, overall, can be difficult to understand. As a consequence, it is difficult to visualize and grasp the security risks presented by them.
Finally, performing security testing for serverless architectures is more complex than testing standard applications. It is, therefore, tempting to look for shortcuts in the testing process to reach aggressive sprint timelines, get to the finish line, and deploy an API.
Consequences of Operating with Insecure SFs (APIs)
Insecure SFs are the root cause of numerous problems such as:
- Business loss. This is directly related to systems downtime and long recovery time.
- Compliance Issues. Depending on geography serving customers with insecure APIs, especially when dealing with PII, may be illegal.
- Reputation loss and brand damage. Even if no data was or money was lost, trust will always be (hopefully not permanently) damaged. This automatically translates into competitor’s advantage.
- Inflated infrastructure bills. Automatic scalability provided by SFs, or any other cloud service for that matter, becomes a nightmare when illegal processes run in the background while using expensive resources uncontrollably.
Common Vulnerabilities And Their Mitigation
Fortunately, it is not all bad news. Being aware of the threats you are against is a great step forward. There are certain easy-to-implement and well-understood practices that you can apply to your serverless APIs to make them more secure.
The 12 Most Critical Risks for Serverless Applications published by the Cloud Security Alliance is a good place to start. Let’s take a quick look at each item on this list:
No Input Validation
This is the most common but most overlooked security mistake. In this scenario, untrusted input is passed to an interpreter before being sanitized and evaluated.
As mentioned earlier, due to their dynamic nature, SFs' input is not tied to HTTP API calls exclusively. They can be triggered by cloud storage events, database events, stream processing, code changes and commits, IoT signals, message queue events, SMS and email messages, etc. This increases the number of input formats exponentially and therefore attack surface.
What you can do…
Trust no input at all! Always sanitize and validate your input employing API parameters or variable binding. Check this article to find out about how to do this in Golang.
Proper treatment of all the inputs sent to an API is vital to ensure an adequate level of security. As defined by the OWASP Foundation, input validation can be applied on two levels: Syntactic and Semantic.
The former is related to the validation of structured fields, i.e. dates, SSNs, currency, etc. Structured values should always be validated against their expected format. On the other hand, semantic validation has to do with the actual value provided by a field, i.e. age, distance, weight, price, etc. cannot be negative.
With this in mind, all input has to be validated against a specific set of rules based on its particular format, e.g. JSON and XML Schemas.
Additionally, strict type conversion and its corresponding exception handling are mandatory. Along with this, using industry-standard input sanitation libraries becomes a necessity to prevent injection attacks.
It is important to note that good old SQL injection is possible even in NoSQL environments. For example, a very common usage of Lambdas is the interaction with DynamoDB (AWS' signature NoSQL database).
A very dangerous scenario occurs when data is accessed using range operators such as “greater than” or “less than” with string attributes. Specifically, searching string columns using any of these operators in combination with wildcard attributes such as “*” or blank may potentially return results that the developer did not intend to return.
The snippet below showcases this scenario:
44// Dangerous use of GreaterThan without proper validation of input value (" " or * will return
45// everything from the table)
46queryFilter := expression.Name("noteId").GreaterThan(expression.Value(apiEvent.NoteId))
Each of your serverless functions should run under the Principle of Least Privilege. This way, if an SF is compromised using harmful input, the damage can be limited to the services and resources the function had access to.
Another factor contributing to poor input validation is the wide range of options available to trigger an SF. Can you figure out the entry/input points that trigger underlying services?
This is easy with traditional APIs, not so much with SFs. Existing DAST and SAST tools to test injection-based vulnerabilities may help to some extent but generally, they do not get along well with SFs.
Threat Modeling can help with this. Enumerate all the possible events that can trigger your serverless functions. All of them are entry points to potentially malicious attacks.
Finally, use a web application firewall to inspect HTTPS traffic, at least. This, however, will not protect against attacks coming from other event trigger types. For AWS Lambdas, a good alternative is to use the API Gateway capabilities to interact with AWS WAF.
Incomplete Or No Authentication
How does your serverless application verify identity? Each API endpoint needs some type of authentication. Plus, each function may need a unique authentication strategy depending on several factors such as:
- Scope (public or internal)
- Input type (storage events, database events, IoT signals, SMS notifications)
This can easily become a security nightmare if not properly implemented. Additionally, the lack of tools for testing serverless authentication aggravates this scenario.
What you can do…
I talked at large about authentication and authorization in my previous post.
The first thing to remember: Do not implement authentication logic. Instead, for internal or user-facing APIs use IAM, Cognito, API Gateway default authorization, Azure App Service Authentication, Google Firebase Authentication, etc. Otherwise, use secure API keys, SAML assertions, client-side certificates, etc.
Use continuous security health check tools such as AWS Config to identify potential dangerous scenarios such as:
- Newly created or recently updated functions
- Improper permissions assigned to functions
- Newly created S3 buckets or potentially dangerous security configurations in existing buckets
Adding authentication to an SF is trivial using the Serverless Framework. See highlighted line in the snippet below:
31functions:
32 createNote:
33 role: createNoteRole
34 handler: bin/createnote
35 events:
36 - http:
37 path: createnote
38 method: post
39 cors: true
40 authorizer: aws_iam
Broken Authorization
Due to their volume and granularity, it is easy to fall in the trap of creating a one-size-fits-all authorization approach for all your SFs leading to functions with unnecessary privileges.
Once again, each of your serverless functions should run under the Principle of Least Privilege.
What you can do…
Take a microservices approach to compartmentalize all the independent functions in your app. This way, each function will have a single responsibility and its own user role. By breaking down a system in as many modules as possible, the blast radius of a breach or attack is greatly diminished.
Also, to grant access to other AWS resources, use IAM roles and policies using Amazon Cognito user pools (see my previous post). This can be easily achieved using the Serverless Framework.
Specifically, any given function will point to the appropriate role which contains only the permissions required by it. See highlighted line below:
31functions:
32 createNote:
33 role: createNoteRole
34 handler: bin/createnote
35 events:
36 - http:
37 path: createnote
38 method: post
39 cors: true
40 authorizer: aws_iam
Then, the role is defined separately. Note that custom roles defined per-function need to take care of additional permissions to create logs and log streams.
1Resources:
2 createNoteRole:
3 Type: AWS::IAM::Role
4 Properties:
5 Path: /awesome/app/createnote/
6 RoleName: CreateNoteRole
7 AssumeRolePolicyDocument:
8 Version: '2012-10-17'
9 Statement:
10 - Effect: Allow
11 Principal:
12 Service:
13 - lambda.amazonaws.com
14 Action: sts:AssumeRole
15 Policies:
16 - PolicyName: createNotePolicy
17 PolicyDocument:
18 Version: '2012-10-17'
19 Statement:
20 - Effect: Allow
21 Action:
22 - logs:CreateLogGroup
23 - logs:CreateLogStream
24 - logs:PutLogEvents
25 Resource:
26 - 'Fn::Join':
27 - ':'
28 -
29 - 'arn:aws:logs'
30 - Ref: 'AWS::Region'
31 - Ref: 'AWS::AccountId'
32 - 'log-group:/aws/lambda/*:*:*'
33 - Effect: "Allow"
34 Resource:
35 - "Fn::GetAtt": [ NotesTable, Arn ]
36 Action:
37 - dynamodb:PutItem
For more information, check the official documentation.
Wrapping Up…
The growth in serverless apps is going to accelerate because the cost and time benefits for developers are overwhelmingly positive. However, this presents the need for a new approach to security with architectures that are substantially different from previous techniques and tools used to secure traditional applications.
In this post, we reviewed the first three aspects that must be considered when implementing more secure serverless functions.
First, we saw the importance of proper input handling as this completely defines the way an API behaves. Then, we analyzed the importance of proper authentication and authorization and how easy it is to implement them using tools like the Serverless Framework. This was extensively covered in my previous post.
This is the first of a short series of posts that will document the basic aspects of securing serverless functions. Stay tuned!
Companion repository for this post can be found at https://github.com/jpcedenog/blog-harden-serverless-api-basics
Thanks for reading!!
Image by TheDigitalWay from Pixabay