Introduction
One of the most exciting new cloud technologies over the last few years has been the emergence of Amazon Web Services’ (AWS) Lambda environment. For the first time, we can now execute a piece of arbitrary code for a specific event such as an upload, database or an API call. This is just the start to the new trend that we’re seeing as we transition from the server-polling models to full event driven serverless cloud architectures. This trend now extends from handling storage, email, database events all the way to building fully functional REST-based APIs without the need to manage servers.
Since Lambda event handlers do not require management of a server process, writing them is a little different than what you might be used to. For instance, there’s no concept of maintaining state inside a Lambda handler. There’s the loading of the handler and handler execution. That’s it. The only downside, you’re billed for the time that your code runs, but many people have found this model cheaper than running servers.
Vandium is an open source Node.js module that makes writing handlers easier while adding validation, robustness and security to your code. From these benefits, one of the overall impacts is that the amount of code you need to write will be greatly minimized. When you write less code, it means there are fewer lines to maintain, thus lowering potential technical debt.
Our Lambda Handler
The following code is an example of a Lambda handler that sends a text message from one user of the system to another.
'use strict';
const tokenValidator = require( './lib/token-validator' );
const db = require( './lib/db' );
const msgService = require( './lib/messages' );
exports.handler = function( event, context, callback ) {
tokenValidator.validateToken( event.token, function( err, result ) {
if( err ) {
return callback( err );
}
let senderId = result.userId;
db.userExists( event.userId, function( err, result ) {
if( err ) {
return callback( err );
}
msgService.send( senderId, event.userId, event.message, function( err, result ) {
if( err ) {
return callback( err );
}
callback( null, 'sent' );
});
});
});
};
Lambda uses the callback
function to capture the final execution state and return that back to the caller. This example is straightforward and it does works, but the drawback is that there’s no validation on the input parameters. This can result in costly calls to the database and service layers, expending excess time and resources.
Validation Time
From a security perspective, validating your inputs is a critical step, as noted by the Open Web Application Security Project (OWASP):
The most common web application security weakness is the failure to properly validate input from the client or environment. This weakness leads to almost all of the major vulnerabilities in applications, such as Interpreter Injection, locale/Unicode attacks, file system attacks and buffer overflows. Data from the client should never be trusted for the client has every possibility to tamper with the data. (source: OWASP/Data_Validation)
Let’s use the following rules for validating our inputs in the event object:
event.userId
must be a UUID - messages should be trimmed, and between 1 and 255 characters.
Our updated Lambda handler now looks like this:
'use strict';
const tokenValidator = require( './lib/token-validator' );
const db = require( './lib/db' );
const msgService = require( './lib/messages' );
const UUID_REGEX = /^[0–9a-fA-F]{8}-[0–9a-fA-F]{4}-[0–9a-fA-F]{4}-[0–9a-fA-F]{4}-[0–9a-fA-F]{12}$/;
exports.handler = function( event, context, callback ) {
if( !event.userId || !UUID_REGEX.test( event.userId ) ) {
return callback( new Error( 'invalid userId' ) );
}
if( !event.message ) {
return callback( new Error( 'missing message' ) );
}
let message = event.message.toString().trim();
if( message.length < 1 ) {
return callback( new Error( 'message is too short' ) );
}
else if( message.length > 255 ) {
return callback( new Error( 'message is too long' ) );
}
tokenValidator.validateToken( event.token, function( err, result ) {
if( err ) {
return callback( err );
}
let senderId = result.userId;
db.userExists( event.userId, function( err, result ) {
if( err ) {
return callback( err );
}
msgService.send( senderId, event.userId, message, function( err, result ) {
if( err ) {
return callback( err );
}
callback( null, 'sent' );
});
});
});
};
As you might notice, when done properly, the code for the validation can be larger than the code that performs the actual intended operation.
Apply some Vandium
The purpose of vandium is to reduce the amount of code you need to write in your handler while seamlessly adding crucial features, such as validation and security. It works simply by defining a set of rules for validation and then wrapping your existing handler.
To install vandium into your project, run the following command in your project’s home directory:
npm install vandium --save
The following code sample is Vandium applied to the text messaging example.
'use strict';
const vandium = require( 'vandium' );
const tokenValidator = require( './lib/token-validator' );
const db = require( './lib/db' );
const msgService = require( './lib/messages' );
vandium.validation( {
userId: vandium.types.uuid().required(),
message: vandium.types.string().min( 1 ).max( 255 ).required()
});
exports.handler = vandium( function( event, context, callback ) {
tokenValidator.validateToken( event.token, function( err, result ) {
if( err ) {
return callback( err );
}
let senderId = result.userId;
db.userExists( event.userId, function( err, result ) {
if( err ) {
return callback( err );
}
msgService.send( senderId, event.userId, event.message, function( err, result ) {
if( err ) {
return callback( err );
}
callback( null, 'sent' );
});
});
});
});
Using vandium, our code got smaller and became more focused on the task of sending the message. In fact, the handler code is exactly the same as our original code (without the validation). This happens because validation is taken care of by vandium before the handler gets invoked.
Vandium does more than simplify your Lambda handler and reduce your code. Inside of vandium’s validation code, it will enforce that the event object contains only those properties that have been specified. This is important because if your code’s functionality changes later on in the development process, and if the validation hasn’t been updated alongside your changed code, Vandium will fail at the handler. This can be very useful for catching issues during testing rather than catching them in a production environment.
Additionally, vandium can do things that you may forget to do or things that you may not have even considered doing, like automatically trimming input string
s. When writing production grade code, tasks like input validation and trimming string
s are tedious and often get missed until something breaks. Vandium’s validation system is designed to take care of this tedious aspect of writing code. Another example of vandium’s validation capabilities is that it can convert string
values to numbers automatically when used with the vandium.types.number()
type. Similar patterns are seen with boolean and binary data.
Detecting Potential Attacks
On top of cleaning your string
s and converting types, vandium will take an additional step. Vandium will analyze all string
s in the event object to determine if the caller is trying to execute an SQL injection (SQLi) attack. Now you might be saying “I don’t use SQL, so I’m not affected” and this is true from an operational perspective, but from a security standpoint identifying that an attack is happening is an important vector. Vandium’s default security setting will report potential attacks by logging to the console.log
.
For example, if a potential attacker called the handler with an event that looks like:
{
"userId": "b08f86af-35da-48f2-8fab-cef3904660bd",
"message": "What's up?\'; drop table users;--"
}
Vandium will detect that the message string
contains the fingerprint of an attack and will report in the console.log
that an attack was attempted. Now in our case, we’re not using an SQL database so we don’t have any worries from a vulnerability perspective.
Dropping Attacks
You might be asking “Do I want to allow a potential attacker to waste my resources?” The answer is probably no. Why waste valuable resources, even though lambda charges are exceptionally low, there’s always a cost when executing services at scale. Since vandium will detect and log the potential attack, you can stop the execution before your handler is called by adding the following line after the validation section:
vandium.protect.sql.fail();
This single line will log and stop the execution of the handler when vandium detects a potential threat, vulnerable or not.
Wrapping Up Part 1 (Pun Intended!)
By using vandium, we were able to quickly add input validation in a non-intrusive manner with code that is simple to use and easy to maintain. Simply by applying vandium to your existing Lambda code, vandium is capable of detecting, logging, and preventing potential SQL Injection attacks.
To be continued in Part Two…
Originally posted on medium.