Securing Credentials

Handling and storing user credentials safely is the first step in improving application security. In this segment we are going to discuss a spectrum of topics, ranging from the most basic form of advice like "don't store plain text passwords", to more advanced topics like why CPU intensive hashing algorithms are the way to go.

Password Policies

So let's start right at the beginning when a user is prompted to enter a password.

This is the point where we can already start implementing security best practices, using specific password rules, called Password Policies.

A password policy is designed to make the password harder to "guess", and prevent brute force and dictionary style attacks.

The strength of a password policy depends on the nature of the business, for example banks usually having much stricter password policies than social media platforms.

Rule examples:

  • Minimum number of characters within a password
  • Maximum number of characters within a password
  • Mix of upper and lower case characters
  • Should include special characters
  • Password must be changed every N days

Storing Passwords

Alright, so I'm just going to come out and shock you with this over the top, controversial statement and say: never, ever store plain text passwords in the database.

Granted, it might be self-evident, but people are still doing it. Why would anybody still be doing it? Because it works. And it takes little to no effort to implement.

Now, let me follow up with something less evident.

Never store passwords in a database.

Not even encrypted passwords.

Alright, I know what you're thinking, but let me elaborate so you get where I'm going with this.

The usual form of encryption (e.g. AES-256) is reversible. It basically uses a secret to encode a block of text (e.g. password), but the problem is that if the attacker gets a hold of the secret, they can decrypt the password. No bueno.

Despite the fact that many organizations — including banks — use it, this approach is not optimal.

The ideal way is a one way transformation of a string, called hashing.

When generating a hash, you take a block of text (e.g. password), a salt (a random string concatenated for added security) and run it through a hashing algorithm. There is no way to "unhash" a string that has been hashed, instead, if you want to validate it, you repeat the procedure and check if the two hashes match.

So hash your passwords and store the hash instead.

We have several general purpose hashing algorithms, some of which you might have heard, e.g. MD5, SHA-1, SHA-256 etc; however, these are very fast on modern CPUs.

Wait. Isn't fast good news? Nope. In this case fast is very bad, because a hacker can run millions of possibilities in a very short amount of time against them. They are not ideal for protection.

So how do we make this bulletproof?

Introducing BCrypt.

BCrypt uses the Blowfish keying schedule, super strong 128-bit salts, hashes a 192-bit value, and runs iteratively.

BCrypt is also CPU intensive and slow, basically the faster the CPU the slower it runs. Which is in fact it's super-power, making a hacker's life very difficult.

So, to summarize, in case you want to maximize credential security:

  • Enforce a strong password policy
  • Do not store passwords in the database (plain or obfuscated)
  • Hash passwords using BCrypt and store the hashes in the database

Storing Secrets

If you're developing servers, you'll unavoidably need to store a secret at some point.

Weather it is a simple OAuth, a JWT secret, a database user, or credentials to another part of your system, you'll have tokens, secrets or ids on your hands that need to be stored securely.

If these secrets get compromised, the possibilities of exploiting your system are endless.

In 2016 two hackers gained access to Uber's AWS account, because the credentials were stored in Uber's GitHub account, stealing personal information of 57 million users and 600,000 drivers.
Uber paid USD 100,000 to the hackers to destroy stolen data, and they lost their valuation by USD 20 billion. (with a 'b')

Secret management DONT's

  • Do not store your secrets in .json files, or anywhere in the file system.
  • Do not push your secrets to a version control system. Your repository might be private, but you might also have 3rd party applications authorized to access the repository. GitHub doesn't offer granular access control over files.

Secret management DO's

  • Always .gitignore your secrets.
  • If you want to version your secrets use Git-Secret. It is a bash tool that encrypts your secret files. It is available for Mac and Linux, uses GNU privacy guard (GPG) and offers granular access control.
  • When using 3rd party services (Heroku, App Engine, Azure, etc.) use environment variables to store secrets in.

Dedicated Secrets Management

Your best bet to manage your secrets are dedicated secret management solution.

Depending on the platform your're running your app on, you'll have multiple options to choose from, including cloud hosted secret management solutions e.g. AWS Param Store, AWS Secrets Manager, Azure Key Vault, Google's Secret Manager etc.

If you're using Docker, you can use Docker Secrets (available when using Swarm)

  • Docker secrets are encrypted using Salsa20Poly1305 encryption with a 256-bit key.
  • They transit securely between nodes using TLS connection.
  • They get mounted into a virtual filesystem for the application in the container to consume.

Other secret management solutions to consider:

  • HashiCorp Vault
    • Enterprise solution for managing secrets.
    • Secrets are stored encrypted at rest in a backend.
    • Secrets are delivered based on access control lists, implementing least privilege.
    • In case of a breach, the vault can be sealed (full lockup, preventing access to all credentials).

  • Square's KeyWhiz
    • An open-source solution for managing secrets.
    • Secrets are stored encrypted at rest in a backend.
    • Secrets are available as pseudo-files on a virtual filesystem that is in-memory.
    • Access control ca be implemented using UNIX file permissions.

Managing Untrusted Data

Blindly ingesting data without integrity checks can leave your app vulnerable to a whole spectrum of attacks.

If you're getting away with just one thing from this section, it should be this: data should never be trusted.

Identifying Untrusted Data

An important prerequisite for securing your applications is to know how to identify security hazards.

When an attacker submits malicious code into your system, it is usually referred to as an Injection Attack. This code is typically disguised as acceptable text, which is submitted with a form or an input to retrieve data or cause damage to the database.

These injection attacks can be broken down into multiple categories.

SQL Injection

When an attacker submits an SQL statement that you unknowingly run on your database, it is called an SQL injection.

Below you can find some SQL injection examples:

  • Submitting the username field with the following format might expose the user, because it disables the conditional part of the query via a comment
After the username the single quotation mark ends the string and the double dash comments out the rest of the SQL query
  • Setting the password with the following format creates and OR condition, bypassing authentication
somepassword' OR '1'='1'
The single quotation mark ends the string, and an OR condition is appended that always evaluates to TRUE
  • Setting a field with the following value might drop the table causing permanent damage
whatever';DROP TABLE users

To detect SQL injection vulnerabilities within your application, you can use the sqlmap open-source tool.

NoSQL Injection

A NoSQL injection has the exact same premise as an SQL injection, but as it's name suggests it is targeting NoSQL databases.

Below you can find an example of a NoSQL injection.

Assuming the server uses something like this in a MongoDB context:

    username: req.body.username,
    password: req.body.password

If you submit the following query string:


It would be interpreted as follows, and the query would run for all the documents that have admin in their username.

    username: { $regex: '^.*(admin).*$, $options: 'im' },
    password: pw

Similar trickery can be performed with the $in operator as well.

Regular Expression Denial of Service (ReDoS)

ReDoS is an injection type that exploits the fact that most Regular Expression implementations may work very slowly in certain edge cases. The slowness of the execution is exponentially related to the input size, so an attacker can cause a program to hang for a very long time by exploiting this weakness.

To avoid regular expression denial of service, use safe-regex.

Cross Site Scripting (XSS)

In case of cross site scripting, malicious code is injected as an input value, which gets stored in the database. It will then be returned and injected to the browser as a rendered value, and it can execute without notice.

Below you can find an example.

   (function xss(){
       // Do something bad
This could theoretically be used as a username, or any other text value

Once the code has been fetched from the database and injected into the DOM, the possibilities of exploitation are endless. The attacker could redirect to a malicious site, steal cookies, install malware or attempt phising attacks against your users.

XSS Attack types:

  • Reflected XSS Attack
    • An app receives data through a HTTP request and includes that data within the immediate response in an unsafe way.
  • DOM-Based XSS Attack
    • Malicious code is interfepted by client the side framework, and displayed on the page. It never goes to the server!
  • Persistent XSS Attack
    • The attacker tries to send the script through the server and insert it into the database. It will execute when the page containing the malicious script is viewed by the victims.

XSS Attacks can be prevented via validation, escaping and sanitization.

Defending Against Malicious Data

XSS Attacks

XSS Attacks are such a serious threat that certain browsers (e.g. Chrome) come with a built-in XSS auditor that tries to neutralize XSS attacks. That said, it is definitely not wise to rely on these browser features as your only layer of security.

In preparation for XSS attacks inbound data must be validated, encoded, and optionally sanitized to neutralize any malicious code that maybe be injected into our application.

For validating the inbound data you can use express-validator.

The outbound data also must be sanitized to prevent persistent XSS attacks from malicious code that may have been stored in the database.

For sanitizing the outbound data you can use express-sanitizer or sanitize-html.

Content Security Policy Header

A content security policy header specifies where the client (browser) is allowed to download resources from (e.g. scripts, fonts, stylesheets, images etc.). This setting limits attackers possibilities as they cannot gain access to external resources.

CSP Example:

    default-src 'self';
    script-src 'self';
    style-src 'self';
    font-src 'self';

To implement these headers, the easies way is to do it via an npm package called helmet.

Example usage of the package:

const helmet = require( 'helmet' );

const app = express();

app.use( helmet.contentSecurityPolicy({
    directives: {
        defaultSrc: ["'self'"]
        styleSrc: ["'self'", ''],
        fontSrc: [ "'self", '', 'data:' ]
        scriptSrc: ["'self'", '' ] // If you're using jQuery ...

Helmet allows you to set up an XSS filter too!

app.use( helmet.xssFilter() );

An another nifty feature is the ability to specify a reporting endpoint, where helmet would send reports if it detects suspicious activity. This is often overlooked, and can be very useful!

CSRF Attacks

Cross-Site Request Forgery (CSRF) attacks occurs when an attacker is able to forge credible requests to a resource.

XSS vulnerabilities make the site more susceptible to CSRF attacks.

Ensure that all transactions and data modifications are permitted ONLY using POST, PUT, or DELETE requests - never use GET requests for transactions.

This prevents an attacker from sending disguised hyperlinks that can cause malicious transactions. GET requests also get stored in the browser's history and can unintentionally be invoked again.

To further improve security we can make sure POST requests have a token associated with them. To achieve this we can rely on express-session and the csurf npm package (we need express-session because the token will be stored in the session).

To protect against clickjacking via iframes, you can use the X-Frame-Options - DENY setting on the request.

This can be done via helmet, or a reverse proxy. I prefer setting this from the webserver config.

Helmet example:

app.use( helmet.frameguard( { action: "deny" } );

Securing Cookies

Cookies are used to store user preferences, logs, and other persistent data. Cookies are also used after authentication to store session data.

If an attacker gets a hold of these session cookies, they are able to impersonate the user, and they can execute all sorts of malicious transactions on the behalf of the user.

Securing cookies is critically important!

To keep your cookies secure use session middleware and manage sessions via cookies e.g. express-session. Do not store important user data in cookies (e.g. usernames), store session ids instead!

To ensure that your cookies are optimally safe, you should understand and set all the fields in the cookie settings.

Cookie settings:

  • secure - TLS only communication
  • httpOnly - Scripts on the site are unable to read cookies
  • domain - Specifying a domain assures that cookies are only sent back to that address
  • maxAge - (TTL time to live) cookie expiration date
  • sameSite - cookies will only be sent to the originating server
  • name - name your cookie so attackers cannot identify the framework by the automatically generated cookie name (e.g. express names its cookies connect.sid)

Always rename the default cookie name when using the express-session middleware.


I think it's safe to say that in today's age HTTPS connection is the standard. Not that long ago, most of the internet was relying on HTTP connections, and the HTTPS protocol was reserved for the big boys only.

In recent years we had access to free certificate authorities like Let's Encrypt, and utilities like Certbot, make it super easy for us to generate certificates!

While SSL/TLS can be set up natively on the node server, I would recommend using a reverse proxy like Nginx instead.

It is hard to overstate the importance of transport security. If TLS is not set up, you're basically sending human-readable text messages.

If HTTPS support is set up, as a last touch make sure to redirect all HTTP (port 80) traffic to HTTPS (port 443).

Optionally you can further harden your TLS security with adding a Diffie-Hellman key. You can use openssl to generate one by executing this command:

openssl dhparam -out /docker-volumes/dh-param/dhparam-2048.pem 2048

To test the configuration of your SSL ciphers, renegotiation, and the validity of your certificate, you can use nmap, sslyze, or test your website via web based SSL scanners.

If you're using Docker you should check out this article, that helps you create SSL certificates in under a minute.

NPM Package Vulnerabilities

With NodeJS and the npm ecosystem we are using several third party packages to speed up our development. These packages then might use other packages, which in turn could use yet other packages.

This is the strength of NodeJS development, but it is also it's weakness, as each of these packages has the potential to introduce security vulnerabilities.

Thankfully, we have tools available for monitoring package vulnerabilities and automatically updating them. One of these is the built-in feature called npm audit.

Whenever you're running npm install the audit feature checks for potential vulnerabilities, and lists them based on their severity.

You can also initiate an audit to get a more detailed report.

npm audit

To apply the fixes simply execute

npm audit fix
--force flag installs breaking changes

Another great solution for identifying vulnerabilities is a tool called Snyk.

Snyk is an open source solution for identifying and fixing vulnerabilities in dependencies.

For using Snyk, you should create a free account on their website, and install the snyk CLI using the global flag

npm install -g snyk

You can then authenticate running

snyk auth

To get a comprehensive report of all your vulnerabilities run

snyk test

To fix the vulnerabilities you can run

snyk wizard

Beyond scanning for vulnerabilities Snyk offers you many different integrations, and you can even set it up to push vulnerability notifications into your Slack channel.

Brute-Force and DDoS Attacks

Both brute-force and DDoS attacks are designed to flood the server with requests, the only major difference being that a brute-force attack is a bit more calculated, trying to gain access to certain resources, while the DDoS is just throwing junk data at the server to overwhelm it.

One way this is done is by infecting millions of machines with malware and gaining control over them, then using those as resources for a targeted attack.

In February 2018 GitHub was hit with 1.35 terabits of traffic per second as part of a DDoS attack. This was the most powerful DDoS attack known to date. Akamai Prolexic was deployed to manage the attack, and it was successfully brought under control, but there were significant outages.
In September 2012 Bank of America, JP Morgan Chase, Wells Fargo, US Bank, and PNC Bank were hit by 60 gigabits of traffic per second and suffered significant outages.

DDoS attacks can be prevented using third party solutions like Cloudflare or Prolexic, or using basic rate limiting practices.

Rate limiting can be implemented to control access to different endpoints/APIs within the system. E.g. a specific endpoint is limited to be called 10 times per second from a single IP

Rate limiting can be implemented natively within NodeJS using the rate-limiter-flexible npm package in conjunction with redis. That being said I would recommend using a web server such as Nginx instead to separate the concerns.