Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Hosted-services / AWS

Simplify and Secure Your Node.js AWS Lambda Code with Vandium — Part 1

4.00/5 (1 vote)
10 Apr 2016CPOL5 min read 9.8K  
First part in a three part series that covers using the open source project vandium to simplify and secure AWS Lambda code

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.

JavaScript
'use strict';

// our token validation service
const tokenValidator = require( './lib/token-validator' );

// our nosql datastore
const db = require( './lib/db' );

// message service
const msgService = require( './lib/messages' );

exports.handler = function( event, context, callback ) {

    tokenValidator.validateToken( event.token, function( err, result ) {
    
        if( err ) {
      
            return callback( err );
        }
    
        // sender id
        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:

JavaScript
'use strict';

// our token validation service
const tokenValidator = require( './lib/token-validator' );

// our nosql datastore
const db = require( './lib/db' );

// message service
const msgService = require( './lib/messages' );

// from wikipedia
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 );
        }
    
        // sender id
        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.

JavaScript
'use strict';

const vandium = require( 'vandium' );

// our token validation service
const tokenValidator = require( './lib/token-validator' );

// our nosql datastore
const db = require( './lib/db' );

// message service
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 );
        }
    
        // sender id
        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 strings. When writing production grade code, tasks like input validation and trimming strings 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 strings and converting types, vandium will take an additional step. Vandium will analyze all strings 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:

JavaScript
{
    "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:

JavaScript
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)