I’ve been writing express apps for about four years. In this blog post, I’ll share some of the best practices when it comes to security in Node.js express applications.
The first thing I do when I come into a Node.js app is look at the package.json and check to see if there is an npm-shrinkwrap.json file. In my experience, it’s best not to rely on semver and version everything off exactly. I’ve been burned too many times in the past relying on ^ and ~ in my package json because unexpected broken dependencies were pulled in and broke my build.
Currently, my .npmrc config looks like this:
save=true save-exact=true progress=false python=/usr/local/bin/python
Notice that save and save-exact are set to true. Now when I install dependencies it saves the exact version and defaults to save. I no longer need to pass --save or --save-exact. When I’m saving a dev dependency I use --save-dev flag. When upgrading, having dev and prod dependencies separated is a reminder of which ones are vital to run the app and will need extra testing.
After I install a new dependency, I create a shrinkwrap file. This is done with npm shrinkwrap --dev. Notice the --dev flag, where shrinkwrap defaults to installing only production dependencies. I version off everything explicitly and make sure the shrinkwrap json includes all of my dependencies. Over the years, this has ensured a bug-free development mode and results in more time our team can dedicate to shipping features.
As soon as I update the shrinkwrap file, I commit the changes. This prevents merge conflicts between team members. Having dependency changes on a long-lived branch should be discouraged, as conflicts in a shrinkwrap file aren’t fun to resolve and can create potential errors.
The package.json should contain the specific node version needed to run the app. There should be something in the readme covering this, and also stating which version of npm is needed. You can easily switch between node versions using nvm. I’ve found it pretty handy to install a default version of node with nvm as well. To switch versions of npm, you can easily reference it to install globally using npm i email@example.com -g by replacing x.x.x with the specific version. Switching versions of node is as easy as nvm use vX.X.X. Many people develop on a different version of node locally than for production; this should be discouraged.
I strongly advise using node long term support (LTS) versions and allowing the npm dependency ecosystem to stabilize before upgrading your production apps to the newest versions. From this blog post, use LTS "if you need stability and have a complex production environment." The community has been great with staying up-to-date, but when a team is busy shipping features, it can be difficult to update to the latest and greatest versions while still maintaining a large project. I suggest designating a member of your team to work with ops to test one server load is balanced before committing the version change across all of your servers.
When choosing dependencies, it’s very important to be familiar with the author and his or her previous work in open source. Npm package.json relies on semver versioning. I’ve fallen victim to using stagnant libraries that have not been updated in years because consultants insist on using home-grown libraries they’ve published to npm or github. Updating deprecated, stale, outdated dependencies is time-consuming and should be avoided. As previously mentioned, using shrinkwrap and removing ~ and ^ will prevent you from falling prey to careless maintainers who publish broken builds.
In the past, Mac users who name their files with camelCase have caused broken builds when tests are run in CI, which runs on Linux; see example here. In a node.js file, require statement camelCase paths can be resolved correctly on a Mac, but not on Linux. For this reason, I always prefer to name my source files in snake_case over camelCase.
When I have sensitive data in my source files, such as db passwords or secret keys, I always use environment variables on my server. This is not node.js specific. For development, I advise using dotenv to manage these sensitive keys / passwords and to have an example_env file in order to show people which credentials they need.
Whenever I’m adding a new feature to my app, I put it behind a feature flag in the feature_flags.js file in the root of my app. This feature flag exports an object where checks can be made to see if a specific feature is enabled or disabled. This is a great post explaining a more advanced usage of feature flags. Using feature flags allows my team to work on features in parallel without worrying about breaking each other’s work. It also prevents long-lived branches, adds the ability to turn features on and off easily, and most importantly, gives me the ability to test a feature in one or two servers before going all in.
In express apps, it’s important to make sure that the input coming from incoming requests is valid. Two libraries I’ve found useful are lodash’s pick method and validate.js combined with bluebird and validator. Using lodash's pick allows your api to ignore all other inputs except for the values needed, and validate.js combined with validator is my go-to combination for all my validation needs, both server-side and client-side. Validate’s extendability and flexible promises / callback api is the primary seller for me (promise lib configurable), and validator has almost every validation method needed. Together, these libraries have met all of my validation needs, from validating emails to validating asynchronously through external services.