Security Considerations

This is not a book on computer security, security is a very broad and complicated topic. Computer security is so broad that it can be separated into each of its specialized topic. Therefore, the scope in which we will cover security is in the context of the web. More specifically, we will be focusing on the application level of our RESTful web API.

When I was building one of my first large scale commercial applications, I really wished someone would have told me, "these are the 80% of security vulnerabilities that you need to protect against, here's a list of them". This would have saved me a bunch of stress and anxiety, and would have definitely made me stop asking the question, "Am I doing this correctly? And will I get hacked?".

In this section, that's exactly what I will be doing. I will be pointing out the top 10 security considerations and guidelines when building your RESTful web API and what you should do to go about protecting against them. The point is to not go in detail about every single possible security concern, but to rather give a general idea as to what you might need to look more into beyond the scope of this book.

The main kinds of attacks we are interested in the context of the web are (No)SQL injection, Man-in-the-middle attack (MITM), Cross Site Scripting (XSS), Cross-site request forgery (CSRF), and Denial-of-service attacks (DDoS).

1. Ensure Proper Access Controls (Authentication & Authorization)

Authentication determines whether someone accessing an API is really who they say they are, whereas authorization determines the level of access and the permissions a particular user has.

Whatever method of authentication you decide to use, just make sure that from the end user's point of view that proper user authentication and privileges are granted accordingly. Although this topic is vast and broad, the general rule of thumb to keep in mind here is to use common sense and to give this security consideration its due diligence.

Take the following example.

// allows the request to go through if the user is authenticated
const isAuthenticated = (req, res, next) => {
  // if the user is authenticated, then next()
  // if not then access is denied
}

app.delete('/api/users/:id', isAuthenticated, async (req, res) => {
  // delete the user with id
})

It may seem as if the isAuthenticated middleware is doing its job, but the developer who wrote this has thought about the aspect of only allowing authenticated users to delete their accounts. The problem though is that any authenticated user may delete any account they choose.

The solution is to add another middleware after the isAuthenticated middleware, let's call it thehasPermissionsToDelete middleware.

// allows the request to go through if the user is authenticated
const isAuthenticated = (req, res, next) => {
  // if the user is authenticated, then next()
  // if not then access is denied
}

// allows the request to go through if and only if the current auth user
// is equal to the request user id
const hasPermissionsToDelete = (req, res, next) => {
  const theUserWeWantToDeleteId = UserModel.findById(req.params.id)
  const theCurrentAuthUserId = req.user.id

  if (theUserWeWantToDeleteId === theCurrentAuthUserId) {
    // we delete the user
  } else {
    // access is denied, you can't delete some one else's user
  }
}

app.delete(
  '/api/users/:id',
  isAuthenticated,
  hasPermissionsToDelete,
  async (req, res) => {
    // delete the user with :id
  }
)

2. SQL Injection

SQL Injection is extremely common and should be a top consideration when building any RESTful backend systems.

Here is a typical example of SQL injection, where the hacker takes advantage of our sloppy insecure select statement and tries to delete our users table.

const mysql = require('mysql')
const pool = mysql.createPool({
  multipleStatements: true
  // ...
})

// Hacker's malicious input
const post_id = "'; DROP TABLE users; --"

// Our SQL query statement here is vulnerable, can you see why?
const statement = `SELECT * FROM posts WHERE id='${post_id}'`

pool.getConnection(function (err, connection) {
  if (err) throw err

  connection.query(statement, function (error, results, fields) {
    if (error) throw error
  })
})

If look more closely and replace the post_id variable, you will see the following statement being executed.

SELECT * FROM posts WHERE id=''; DROP TABLE users; --'

There are plenty of ways to defend against this type of attack, such as using prepared statements, escaping the input, or black listing input. You can find additional details in this awesome SQL Injection Prevention Cheat Sheet which gives you a bunch of options for defending against SQL injection attacks.

For our example, we can do a simple raw check on the post_id variable, like this.

const mysql = require('mysql')
const pool = mysql.createPool({
  multipleStatements: true
  ///...
})

// Hacker's malicious input
const post_id = "'; DROP TABLE users; --"

// Our SQL query statement here is vulnerable, can you see why?
let statement = `SELECT * FROM posts WHERE id='${post_id}'`

// Our simple and raw protection against SQL injection
if (!Number.isInteger(post_id)) {
  throw Error('Post ID is not the correct type!')
}

pool.getConnection(function (err, connection) {
  if (err) throw err

  connection.query(statement, function (error, results, fields) {
    if (error) throw error
  })
})

Note: there's also NoSQL Injection. Since the NoSQL movement has been quite new in the recent years, there are actually less protections against NoSQL injection versus just plain old SQL injection attacks. However, the most tiresome aspect of NoSQL databases and protecting against NoSQL injection attacks is that because there isn't a common language among NoSQL databases, protecting and testing requires knowledge specific to the database, syntax, data model, and their APIs. In fact, according to https://nosql-database.org, there are over more than 225 different types of NoSQL databases on the market. With that being said, it is therefore important to also consider protecting against NoSQL injection attacks depending on the database you choose to use in your application.

3. Encrypt Sensitive Information in the Database

Sensitive information that you do not want malicious hackers to know about should be at the very least encrypted before they hit your database. Sensitive information can be anything from passwords, credit card numbers, and social security numbers. Whatever you choose to store in your database, it's always best to have some level of encryption of sensitive information.

Here is a great example of how most people store hashed passwords, we'll be using the built-in crypto library in node as well. This may not be the best way of doing things, but it is a way.

const crypto = require('crypto')

/* Helpers */

function generateSalt() {
  return crypto.randomBytes(16).toString('base64')
}

function generateHash(password, salt) {
  var hash = crypto.createHmac('sha512', salt)
  hash.update(password)
  return hash.digest('base64')
}

/** These are our auth routes **/

app.post('/register', async (req, res, next) => {
  const username = req.body.username
  const password = req.body.password

  const salt = generateSalt()
  const hashedPassword = generateHash(password, salt)

  // When the user registers, we store both the randomly
  // generated salt as well as the hashed password
  await UserModel.update(
    { _id: username },
    { _id: username, password: hashedPassword, salt },
    { upsert: true }
  )
})

app.post('/login', async (req, res, next) => {
  const username = req.body.username
  const password = req.body.password

  // Retrieve user from the database
  const user = await UserModel.findOne({ _id: username })

  // When the user tries to login, we compare the generated hash
  // which is produced from the user's input password along with
  // the generated salt
  if (user.password === generateHash(password, user.salt)) {
    // correct credentials, logs the user in...
  } else {
    // incorrect credentials, does not log the user in...
  }
})

4. Encryption Sensitive Data in the URI

On the topic of encryption, in order to prevent man-in-the-middle attacks to our API, it's important to encrypt sensitive data in the URI. Better yet, to not send any sensitive data in the URI in the first place. If you do need to leave in sensitive data in the URI, make sure to encrypt it, because TLS will not prevent hackers from sniffing and intercepting HTTP, or rather HTTPS calls, if the data is in the URI. Recall that HTTPS will only protect and encrypt information in the header, not the URI.

Below is an example of what NOT to do.

GET https://www.website.com/api/social-security-number/078-05-1120

Here is a way better version of the above example.

GET https://www.website.com/api/social-security-number/$6$FP1fYsh4CiH_rest_of_the_hash

5. Prevent Cross-Site Request Forgery with CSRF Tokens

CSRF (Cross Site Request Forgery ) requires quite an extensive amount of explanation and a little beyond the scope of this book. To learn more in depth, below is the best article I have ever read on the topic of CSRF written by auth0.com. This is especially important if you are using any sort of session based authentication in your application.

6. Protect Your Cookies!

I've always been skeptical about the security of using cookies, but the main big 3 concepts about cookies in the context web security can be broken down to these 3 flags, HttpOnly, Secure, and SameSite. Below is a brief summary of these 3 flags and what their purposes are. My suggestion is to look more in depth into these 3 security mechanisms and to play around to see how they work, because simply reading about it is not enough to truly understand them.

HttpOnly flag against XSS

The HttpOnly flag makes sure that our cookie cannot be read by arbitrary JavaScript code. If you were to go into the chrome console to any website and type into the console document.cookie, you will notice that the ones with a check mark in the secure column will not show up. This is to protect against the ability for any remote code from executing, you can see that this is especially useful to protect cookies such as session ids.

Secure flag against MITM

The Secure flag is quite straight forward. Essentially, it instructs the browser to re-attach cookies with HTTPS requests only (and not plain HTTP ones). This can be used to any potential mixed-content vulnerability that would leak a cookie value.

SameSite flag against CSRF

Cookies can sent to third parties with cross-origin requests. This can be abused by CSRF attacks. One way to avoid specific cookies to be sent with cross-origin requests is to set a special flag called SameSite. This will make it so that cookies can only be forwarded from the same domain origin.

7. Take Advantage of Cross Origin Resource Sharing

Have you ever tried calling your API from the client side and you got a Cross Origin Resource Sharing (CORS) issue?

I'm talking about doing this.

app.get('/resource', (req, res) => {
  // Don't do this if you don't want every other
  // application in the world to call you API
  res.set('Access-Control-Allow-Origin', '*')

  res.json({
    message: 'Hey there!'
  })
})

Instead, a more "white-listing" approach is what you should be doing.

app.get('/resource', (req, res) => {
  // This is better!
  res.set('Access-Control-Allow-Origin', 'https://my-website-client.com')

  res.json({
    message: 'Hey there!'
  })
})

For better management of these headers, use the cors package.

Another key point to remember is that CORS is a browser mechanism that prevents one domain origin from access another domain origin's response output. It does not actually prevent anything happening in the background when an API is called. So if you an API endpoint that is not safe, such as deleting a user in the database, don't expect CORS to help you prevent that.

8. Sanitizing All Input Data

It is important to always remember to never trust the client's input. Always be on the look out to sanitize all input data given by the consumer of our API. Escaping and filtering input are the most common ways to defend against malicious user inputs.

One of the most popular libraries in the node community is the validator package.

Although general and raw techniques of sanitizing data are good, I recommend using a package such as this one to further the defensive mechanism of your RESTful API, especially when it comes to escaping data.

9. Use the Content Security Policy (CSP) Header To Prevent XSS

We just talked about how to never trust input, but you also can never trust the output. Imagine you have a list of comments on your website that you want to display to the public, those comments are of course made by the users themselves. You can decide to both sanitize the input and also escape the output when rendering the data to the screen.

There is actually a more sophisticated way of doing this "output escape" from your API, and that is using the CSP header. You can read more about it on MDN.

Here's an example, when we request a comment by an id and we want to output it as a JSON response for which the client wants to consume, we send the following header to the client Content-Security-Policy: script-src 'self'. This header tells the browser to protect any malicious JavaScript from executing in the browser.

app.get('/comments/:id', (req, res) => {
  res.setHeader('Content-Security-Policy', "script-src 'self'")

  const comment = CommentModel.findOne({ id: req.params.id })

  res.json({
    data: comment.body
  })
})

For more in depth look at how CSP works, I recommend this YouTube video: Content Security Policy by Kyle Robinson Young.

10. Prevent Denial-of-service Attacks by Rate Limiting Requests

If you have a very popular and public application, you might want to think about protecting against DDoS attacks in order to maintain high availability of your API. The most common way to do so (at least for developers) is to add a rate limiter in your RESTful API.

Ideally, we prevent Denial-of-service attacks (DDoS) from outside the application level, so the attacker can never even reach our application. However, since this is not a book on systems administration and server configuration management, we'll just have to put a rate limiter directly in the code of our application.

For this I recommend the express-rate-limit package. Here is a simple sample code on how it works.

const rateLimit = require('express-rate-limit')

// This rate limiter will only allow 100 requests per 1 minute from
// the same IP address.
const apiLimiter = rateLimit({
  windowMs: 1 * 60 * 1000, // 1 minute intervals
  max: 100
})

// Only apply to requests that begin with /api/
app.use('/api/', apiLimiter)

Last updated