Analyze Your Application’s Data Flow

At this point in the process, you know your code doesn’t have major quality issues that could cause breakages. Let’s move on to deeper analysis.

The most effective way to secure your application is to first understand it. You must grasp how your application does what it does, and to do that you must follow the data.

Input/output (I/O) operations are the core of any web application and are something Node.js excels at. But what’s actually going on? See the following graphic.

images/data-flow-no-understanding.png

You need to understand why your application behaves in a certain way and how it handles your requests. That’s the only way you’ll know all the possible permutations of what the application can do, and you can limit the list accordingly. An in-depth understanding of the application also helps you narrow your search area when hunting for vulnerabilities.

To start, you can narrow your search field by grouping request handling into various categories: static requests, insecure and secure data requests, content-modifying requests, and client-side variables, as shown in this diagram.

images/analysis.png

Let’s look at each of them in detail.

Identify Static Requests in Your Code

Static requests have no user input besides the URL path. This includes static files and paths that serve generic content, such as the home page and login page. While the pages served can be dynamic, such as showing the latest five stories, they shouldn’t rely on user input to generate the contents displayed. These requests don’t have session data, so they’re public by definition.

A path generating dynamic content like this is static:

 function​ getRandomNumbers() {
 var​ randoms = [];
 for​(​var​ i = 0; i < 5; i++) {
  randoms.push(Math.random());
  }
 return​ randoms;
 }
 
 app.get(​'/'​, ​function​ (req, res) {
  res.json(getRandomNumbers());
 });

A path serving static content like this is not because it checks for the user’s logged-in status based on the session, which is based on a cookie, a type of user input:

 var​ session = require(​'express-session'​);
 var​ cookieParser = require(​'cookie-parser'​);
 var​ easySession = require(​'easy-session'​);
 
 app.use(cookieParser());
 app.use(session({
  secret: ​'this is a nice secret'​,
  resave: ​false​,
  saveUninitialized: ​true
 }));
 app.use(easySession.main(session));
 
 app.get(​'/login'​, ​function​ (req, res) {
 if​(req.session.isLoggedIn()) {
  res.redirect(​'/'​);
 return​;
  }
  res.send(​'<form></form>'​);
 });

You don’t want the user to be able to force the server into using the input in any way. Static resource serving is commonly targeted with path traversal attacks. This is why you need to be concerned with those paths and secure them as needed.

Identify Insecure and Secure Data Requests in Your Application

Insecure data requests are requests for dynamic content that don’t need authorization. These requests use GET and HEAD requests as well as path, query, and cookie parameters to determine what content to serve on the page. Since the pages serve only public content, the user doesn’t have to worry about authentication.

The following example shows a data request that’s insecure because it uses the user-provided value to display data. The problem is, as written, it lets an unauthorized user provide that information:

 app.get(​'/:path'​, ​function​ (req, res) {
  res.sendFile(req.params.path + ​'.html'​);
 });

You can use session variables as long as you aren’t relying on the user’s identity. Let’s look at two versions of the same code, one secure and the other not:

 // This is an insecure request
 app.get(​'/session'​, ​function​ (req, res) {
 if​(!req.session.nr || ​typeof​ req.session.nr !== ​'number'​) {
  req.session.nr = 0;
  }
  req.session.nr++;
  res.send(​'Request nr: '​ + req.session.nr);
 });
 
 // This is not an insecure request
 app.use(easySession.main(session));
 app.get(​'/login'​, ​function​ (req, res) {
 if​(req.session.isLoggedIn()) {
  res.redirect(​'/'​);
 return​;
  }
  res.send(​'<form></form>'​);
 });

Once you’ve identified insecure data requests in your application, the best thing to do is to whitelist all user input wherever possible. This is the single most effective protective measure you can take with these paths:

 var​ allowedFiles = [
 'index'​,
 'login'​,
 'static'
 ];
 app.get(​'/:path'​, ​function​ (req, res) {
 // Validate that it is an expected path
 if​(allowedFiles.indexOf(req.params.path) === -1) {
  res.send(404);
 return​;
  }
  res.sendFile(req.params.path + ​'.html'​);
 });

In fact, you should go through the handler’s call stack and make sure every function relying on user input or a value based on user input has embedded whitelist checks. You’ll have to use other sanitizing methods if whitelisting isn’t an option, but you still want to limit all possible inputs.

Perform sanity checks on all variables that are part of the request, including the ones you introduce via cookies as well as those created by client-side code. When introducing sanitizing methods, take into account the locations and functions using the variables, such as the database, the file system, and the command line, because you’ll have to use the methods differently depending on location.

Don’t forget about second-hand validation; you need to validate data previously inserted into storage by users and then retrieved. Let’s look at web comments, since they’re one of the most common examples of second-hand input. Users post comments from the application, and the server saves the data in storage. When the page is displayed later, the application may look at the timestamp to retrieve and display some of the comments. These comments need to be validated.

You also need to look at secure data requests. These are similar to the insecure requests, except they’re used to serve restricted data. The output depends on the user’s identity. You perform the same checks on secure data requests as you do on insecure data requests. Make sure the user’s identity and access level match what is being requested. This is a good time to review access checks as discussed in Chapter 9, Set Up Access Control.

Identify Content-Modifying Requests in Your Application

The next group is the one most prone to errors and requires thorough checking. These requests modify or store information in your application and change the application’s state. We’re talking about PUT, POST, PATCH, and DELETE requests. We’ve discussed some of the key factors already: checking access rights for secure requests, limiting input data, validating input data based on location, and being vigilant for errors. What you also need to do is to check for the request’s origin to prevent CSRF. You can review how to prevent CSRF attacks in Chapter 12, Avoid Request Forgery.

When accepting input from the client, be as strict as possible. This means that when you expect a path to process POST requests you do not accept GET or any other type besides POST. The same goes for input variables—do not use generic parameter access methods like req.param() that take input from the path, body, or query depending on where it’s found first. For example, if you expect a POST with a body, then don’t accept query or path parameters as substitutes. Doing so would create confusion and in some cases allow attackers to exploit the order in which validation and usage are performed.

Users can always add unexpected parameters to requests. Remove these before running the rest of the code.

Nothing in the following code prevents the attacker from adding the role parameter to the request being submitted. Even though the role isn’t one of the values included in the form, it doesn’t matter because the attacker can create administrator users with the parameter:

 // Define user model
 var​ userSchema = ​new​ mongoose.Schema({
  username: { type: String, required: ​true​, index: { unique: ​true​ } },
  password: { type: String, required: ​true​}, ​// this should be hashed
  role: {
  type: String,
  enum: [​'guest'​, ​'user'​, ​'admin'​],
  required: ​true​,
  default: ​'user'
  }
 });
 
 var​ User = mongoose.model(​'User'​, userSchema);
 
 app.post(​'/user'​, ​function​ (req, res) {
  User.create(req.body, ​function​ (err, user) {
 if​(err) {
  console.log(err);
  res.send(500);
 return​;
  }
  res.send(200);
  });
 });

In order to prevent these kinds of data modifications, you should clean the input to allow only specified variables. After you remove the extra variables, you should still sanitize what’s left:

 var​ allowed = [
 'username'​,
 'password'
 ];
 app.post(​'/user'​, ​function​ (req, res) {
 var​ data = {};
 
 //Filter the input
  allowed.forEach(​function​ (key) {
  data[key] = req.body[key];
  });
 
  User.create(data, ​function​ (err, user) {
 if​(err) {
  console.log(err);
  res.send(500);
 return​;
  }
  res.send(200);
  });
 });

Identify and Clean Client-Side Variables Used by Your Application

In the last step, we look at identifying all the variables that affect how client-side code is constructed and clean them to prevent XSS attacks. If your application is just an API, you don’t need to worry about this last category. Otherwise, XSS is a major attack vector and must be mitigated. Now is a good time to go over the template and client-side JavaScript files while keeping the lessons from Chapter 11, Fight Cross-Site Scripts in mind.

It will be a long and tedious process, but the result should be a secure client for your web app. Employing these methods will make server-side code easier to understand, straightforward to maintain, and harder to attack. So far, we’ve focused on securing your existing codebase. Let’s look at the techniques you’ll need for securing third-party code, the modules and libraries written and maintained by someone else.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.18.4