Categories
Security

More Security Best Practices for Using Express Apps in Production

When we use an Express app for production usage, i.e. it’s used by external users, we have to be careful about security since it’s available to the outside world.

In this article, we’ll look at some security best practices when running Express apps in a production environment.

Ensure Dependencies are Secure

We can check if the Node packages we use in our apps are secure by using NPM’s automatic security check when we install packages.

We can also run manual security checks with npm audit.

Both commands check the Node packages we use for security vulnerabilities.

To stay even more secure, we can use Snyk. It offers a command-line tool and Github integration to check our repositories automatically against a list of security vulnerabilities that Snyk has.

It’s available as a Node package. To install it, we run npm i -g snyk.

We can use snyk test to test our apps for vulnerabilities.

snyk wizard is a command-line wizard that walks us through the process of applying updates to fix vulnerabilities that were found.

Avoiding Other Known Vulnerabilities

We can check the Node Security Project website or Snyk advisories that may affect Express and other Node modules that we use in our app.

These databases are great resources for learning about Node security.

We also have to be aware of general web vulnerabilities that are in the wild on the web.

Cross-Site Request Forgery

Cross-site request forgery (CSRF) is a vulnerability where an attack can make unauthorized requests to our app as the user that our app trusts.

Attackers can do this in many ways like specially crafted commands, image tags, hidden forms, JavaScript XMLHttpRequests, etc.

We can use the csurf middleware to protect our app from CSRF attacks. It adds a req.csrfToken() function so that we can check against the CSRF token when we handle requests to make sure that the request is actually coming from users we trust.

Filter and Sanitize Inputs

We should filter and sanitize inputs so that attackers can’t run code in our system.

Sanitizing inputs help with this since strings with special characters may be directly injected into code in some places.

To prevent input strings from having special characters that are part of some malicious code snippet, we should escape those characters so they’re turned in more harmless characters.

SQL Injection

Preventing SQL injection attacks is another reason to sanitize inputs. Not only that we should sanitize inputs, but we should also make sure that input strings are never directly injected into SQL statements.

If we let strings be interpolated straight into SQL commands, then attackers can run anything they wish.

We definitely don’t want that to happen because they may run commands to steal, data, corrupt data, destroy our database and more.

To prevent SQL injection, we should use parameterized queries and prepared statements.

Prepared statements will sanitize inputs before running them so that we don’t have to interpolate values into SQL command strings directly.

For example, if we use SQLite as our database in our Express app, we can write something like:

app.post('/', (req, res) => {  
    const { name, age } = req.body;  
    db.serialize(() => {  
        const stmt = db.prepare('INSERT INTO persons (name, age) VALUES (?, ?)');  
        stmt.run(name, age);  
        stmt.finalize();  
        res.json(req.body);  
    })  
})

In this case, name and age are sanitized before the prepared statement:

INSERT INTO persons (name, age) VALUES (?, ?)

is run.

name and age are sanitized and replace the question marks when we run:

stmt.run(name, age);

db.prepare is a method in the SQLite3 package. Other database libraries should also support prepared statements.

We can use the sqlmap tool to detect SQL injection vulnerabilities in our app.

Testing TLS Security

We should use the nmap and sslyze tools to test the configuration of our SSL /TLS ciphers, keys, renegotiation, and validation of our certificates.

nmap is a utility for network discovery and security audits. It does a variety of scans to check for vulnerabilities in our systems.

sslyze is used specifically for SSL/TLS scanning to check for configuration issues with our SSL/TLS setup.

Check for Regular Expression Denial of Service Attacks

Regular expression denial of service attacks (ReDoS) is an attack that exploits the fact that most regex implementations may reach extreme situations to make them work extremely slowly.

Attackers can send inputs that make our app that enter these extreme situations and make our app hang.

Grouping with repetition of various kinds are examples of regexes that may be subject to this kind of vulnerability.

We can use safe-regex to check that our regexes aren’t susceptible to (ReDos) attacks.

Conclusion

When we put our Express app in a production environment, there’s more to think about than just getting our app to work.

We have to be aware of SQL injection attacks and other code injection attacks so that our apps won’t run malicious code. To do this, we sanitize inputs and run SQL queries as prepared statements.

Also, we have to be aware of SSL/TLS security and check if our server configuration is secure by using various utilities to check our server configuration.

Our app’s dependencies should be checked for vulnerabilities so that we can update them to avoid the security issues.

Finally, we should make sure the regexes that we use aren’t vulnerable to (ReDoS) attacks, which may bring down our app.

Categories
Security

Security Best Practices for Using Express Apps in Production

When we use an Express app for production, i.e. it’s used by external users, we have to be careful about security since it’s available to the outside world.

In this article, we’ll look at some security best practices when running Express apps in a production environment.

Don’t Use Outdated Versions of Express

Outdated versions of Express are no longer maintained and may have unpatched security vulnerabilities, leaving our apps at risk for all sorts of attacks.

Use TLS

If we’re transporting data that needs to be secured, we should use TLS so that users’ data is protected. This means that we avoid man-in-the-middle attacks and packet sniffing.

Even our apps don’t transport secure data, it still gives users more confidence that our website is secure.

We can get free TLS certificates with Let’s Encrypt. It’s an automated service that generates new certificates that are trusted by browsers,

Use Helmet

Helmet is a series of Express middlewares to protect our apps with some well-known security vulnerabilities on the web by setting HTTP headers.

The middlewares included with Helment includes:

  • csp — sets the Content-Security-Policy header to help prevent cross-site scripting attacks and other cross-site injections.
  • hidePoweredBy — removes the X-Powered-By header
  • hpkp — adds public key pinning header to prevent man-in-the-middle attacks with forged certificates
  • hsts — sets Strict-Transport-Security header that enforces HTTPS connects to the server.
  • ieNoOpen 0 sets X-Download-Options for IE 8 or later
  • noCache — sets Cache-Control and Pragma header to disable client-side caching
  • noSniff — sets X-Content-Type-Options header to prevent browsers from MIME-sniffing a response away from the declared content type
  • frameguard — sets the X-Frame-Options header to provide clickjacking protection
  • xssFilter — sets X-XSS-Protection to enable cross-site scripting filter in most recent web browsers

It’s a good idea to at least disable the x-powered-by response header so that attackers won’t know that our app is an Express app and launch specific attacks for it.

Use Cookies Securely

We can use the express-session or cookie-session middleware to use cookies securely.

express-session stores session data on the server. It only saves the session ID on the cookie itself.

We can set up a session store for production use since it uses an in-memory store by default.

cookie-session stores the whole cookie on the client-side. We shouldn’t exceed 4093 bytes when we set our cookies.

The cookie data will be visible to the client, so secret data shouldn’t be sent to the client with cookie-session .

Don’t Use Default Session Cookie Name

The default session cookie name set by Express shouldn’t be used since it can identify that an app is running on Express.

We can do this by passing in an object to session as follows:

const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.set('trust proxy', 1);
app.use(session({
  secret: 'secret',
  name: 'sessionId'
}))

app.listen(3000);

Set Cookie Security Options

To enhance security, we can set various cookie options. The following options are available:

  • secure — ensures browsers only send cookie over HTTPS
  • httpOnly — ensures cookie is sent only over HTTP(S) and not client-side JavaScript. This helps us prevent XSS attacks
  • domain — indicates the domain of the cookie, which can be used to compare against the domain of the server in the URL that’s requesting the cookie.
  • path — indicates the path to compare against the requested path, then send the cookie in the request
  • expires — set the expiration date for persistent cookies.

For example, we can use it as follows:

const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const expiryDate = new Date(Date.now() + 60 * 60 * 1000)
app.use(session({
  name: 'session',
  secret: 'secret',
  keys: ['foo', 'bar'],
  cookie: {
    secure: true,
    httpOnly: true,
    domain: 'EnormousBeneficialScript--five-nine.repl.co',
    path: '/',
    expires: expiryDate,
  }
}));

app.get('/', (req, res) => {
  req.session.foo = 'foo';
  res.send(req.session.foo);
})

app.listen(3000);

Prevent Brute-Force Attacks

We can rate limit our authorization routes so that attackers can’t use brute-force attacks to guess the credentials stored in our app’s database.

For example, we can limit by IP address by limiting the number of failed attempts that people can make if they have the same user name and IP address.

Then we lock out the user for some time if the number of failed attempts what we can tolerate.

If more than 100 failed attempts in a day are made then we block the IP address.

We can use the rate-limiter-flexible package to do rate-limiting of our routes.

Conclusion

To keep our Express app secure in a production environment, we should take a few precautions.

First, we should use TLS for transporting data to keep them from being sniffed and prevent man-in-the-middle attacks.

Next, we should set headers in ways that prevent users from finding information about our app and to enforce secure communication.

Also, we should use cookies securely by signing it with a secret and set the security options for them.

Finally, to prevent brute-force attacks, we should set a rate limit for our API calls so that attackers can’t guess our users’ credentials by repeated login attempts.

Categories
MongoDB

Using MongoDB with Mongoose — Model Validation

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Validation

Mongoose comes with validation features for schemas.

All SchemaTypes have a built-in validator.

Numbers have min and max validators.

And strings have enum, match, minlength, and maxlength validators.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const breakfastSchema = new Schema({
    eggs: {
      type: Number,
      min: [6, 'too few eggs'],
      max: 12
    },
    bacon: {
      type: Number,
      required: [true, 'too few bacon']
    },
    drink: {
      type: String,
      enum: ['orange juice', 'apple juice'],
      required() {
        return this.bacon > 3;
      }
    }
  });
  const Breakfast = connection.model('Breakfast', breakfastSchema);
  const badBreakfast = new Breakfast({
    eggs: 2,
    bacon: 0,
    drink: 'Milk'
  });
  let error = badBreakfast.validateSync();
  console.log(error);
}
run();

We create the Breakfast schema with some validators.

The eggs field have the min and max validators.

The 2nd entry of the min array has the error message.

We have similar validation with the bacon field.

The drink field has more validation. We have the required method to check other fields to make this field required only if this.bacon is bigger than 3.

enum has the valid values for the drink field.

Therefore, when we create the Breakfast instance with invalid values as we in the code above, we’ll see the errors after we run the validateSync method.

The messages are in the message property in the errors object.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const breakfastSchema = new Schema({
    eggs: {
      type: Number,
      min: [6, 'too few eggs'],
      max: 12
    },
    bacon: {
      type: Number,
      required: [true, 'too few bacon']
    },
    drink: {
      type: String,
      enum: ['orange juice', 'apple juice'],
      required() {
        return this.bacon > 3;
      }
    }
  });
  const Breakfast = connection.model('Breakfast', breakfastSchema);
  const badBreakfast = new Breakfast({
    eggs: 2,
    bacon: 0,
    drink: 'Milk'
  });
  let error = badBreakfast.validateSync();
  console.log(error.errors['eggs'].message === 'too few eggs');
}
run();

to get the error as we did in the last line of the run function.

The unique option isn’t a validator. It lets us add unique indexes to a field.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const uniqueUsernameSchema = new Schema({
    username: {
      type: String,
      unique: true
    }
  });
}
run();

to add a unique index to the usernamd field.

We can also add a custom validator. For instance, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const userSchema = new Schema({
    email: {
      type: String,
      validate: {
        validator(v) {
          return /(.+)@(.+){2,}.(.+){2,}/.test(v);
        },
        message: props => `${props.value} is not a email!`
      },
      required: [true, 'Email is required']
    }
  });
  const User = connection.model('User', userSchema);
}
run();

We add the email field to with the validate method with the validator function to add validation for the email field.

The message method is a function that returns the error message if validation fails.

Conclusion

We can add validation in various ways with Mongoose.

Categories
MongoDB

Using MongoDB with Mongoose — Subdocuments

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Parents of Subdocuments

We can get the parent document from a child document.

For example, we can write:

async function run() {
  const mongoose = require('mongoose');
  const connection = mongoose.createConnection('mongodb://localhost:27017/test');
  const childSchema = new mongoose.Schema({ name: 'string' });
  const parentSchema = new mongoose.Schema({
    children: [childSchema],
    child: childSchema
  });
  const Child = await connection.model('Child', childSchema);
  const Parent = await connection.model('Parent', parentSchema);
  const parent = new Parent({ child: { name: 'Matt' }, children: [{ name: 'Matt' }] })
  await parent.save();
  console.log(parent === parent.child.parent());
  console.log(parent === parent.children[0].parent());
}
run();

Then both console log statements are true because the parent method returns the parent object of the child and a subdocument in the children fields.

If we have a deeply nested subdocument, we can call the ownerDocument method to get the root document:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const parentSchema = new Schema({
    level1: new Schema({
      level2: new Schema({
        test: String
      })
    })
  });
  const Parent = await connection.model('Parent', parentSchema);
  const doc = new Parent({ level1: { level2: { test: 'test' } } });
  await doc.save();
  console.log(doc === doc.level1.level2.ownerDocument());
}
run();

We call the ownerDocument method with on the level2 subdocument to get the root document.

Therefore, the console log should log true .

Alternative Declaration Syntax for Arrays

We can declare nested documents in more than one way.

One way is to put the array straight into the schema object:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const parentSchema = new Schema({
    children: [{ name: 'string' }]
  });
  const Parent = await connection.model('Parent', parentSchema);
  const doc = new Parent({ children: { name: 'test' } });
  await doc.save();
}
run();

We can also create a schema with the Schema constructor:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const parentSchema = new Schema({
    children: [new Schema({ name: 'string' })]
  });
  const Parent = await connection.model('Parent', parentSchema);
  const doc = new Parent({ children: { name: 'test' } });
  await doc.save();
}
run();

Alternative Declaration Syntax for Single Nested Subdocuments

There are also 2 ways to declare subdocuments schema.

One way is to write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({
    nested: {
      prop: String
    }
  });
  const Parent = await connection.model('Parent', schema);
}
run();

or:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({
    nested: {
      type: new Schema({ prop: String }),
      required: true
    }
  });
  const Parent = await connection.model('Parent', schema);
}
run();

Both ways will set the nested subdocument with the data type string.

Conclusion

We can work with MongoDB subdocuments in different ways with Mongoose.

Categories
MongoDB

Using MongoDB with Mongoose — Queries and Aggregations

To make MongoDB database manipulation easy, we can use the Mongoose NPM package to make working with MongoDB databases easier.

In this article, we’ll look at how to use Mongoose to manipulate our MongoDB database.

Cursor Timeout

We can set the noCursorTimeout flag to disable cursor timeout.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({
    name: {
      first: String,
      last: String
    },
    occupation: String
  });
  const Person = connection.model('Person', schema);
  const person = new Person({
    name: {
      first: 'james',
      last: 'smith'
    },
    occupation: 'host'
  })
  await person.save();
  const person2 = new Person({
    name: {
      first: 'jane',
      last: 'smith'
    },
    occupation: 'host'
  })
  await person2.save();
  const cursor = Person.find({ occupation: /host/ })
    .cursor()
    .addCursorFlag('noCursorTimeout', true);
  let doc;
  while (doc = await cursor.next()) {
    console.log(doc);
  }
}
run();

We call the addCursorFlag to disable the cursor timeout.

Aggregation

We can call aggregate to do aggregation with the results.

This will return plain JavaScript objects rather than Mongoose documents.

For example, we can write:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({
    name: {
      first: String,
      last: String
    },
    occupation: String
  });
  const Person = connection.model('Person', schema);
  const person = new Person({
    name: {
      first: 'james',
      last: 'smith'
    },
    occupation: 'host'
  })
  await person.save();
  const person2 = new Person({
    name: {
      first: 'jane',
      last: 'smith'
    },
    occupation: 'host'
  })
  await person2.save();
  const results = await Person.aggregate([{ $match: { 'name.last': 'smith' } }]);
  for (const r of results) {
    console.log(r);
  }
}
run();

We call aggergate to get all the entries that has the name.last property set to 'smith' .

Then we use a for-of loop to loop through the items since it returns a regular JavaScript array.

The aggregate method doesn’t cast its pipeline, so we have to make sure the values we pass in the pipeline have the correct type.

For instance, if we have:

async function run() {
  const { createConnection, Schema } = require('mongoose');
  const connection = createConnection('mongodb://localhost:27017/test');
  const schema = new Schema({
    name: {
      first: String,
      last: String
    },
    occupation: String
  });
  const Person = connection.model('Person', schema);
  const person = new Person({
    name: {
      first: 'james',
      last: 'smith'
    },
    occupation: 'host'
  })
  await person.save();
  const person2 = new Person({
    name: {
      first: 'jane',
      last: 'smith'
    },
    occupation: 'host'
  })
  await person2.save();
  const doc = await Person.findOne();
  const idString = doc._id.toString();
  const queryRes = await Person.findOne({ _id: idString });
  console.log(queryRes);
}
run();

queryRes isn’t cast to the Person since it’s a plain JavaScript object.

Query Methods

Mongoose comes with the following query methods:

  • Model.deleteMany()
  • Model.deleteOne()
  • Model.find()
  • Model.findById()
  • Model.findByIdAndDelete()
  • Model.findByIdAndRemove()
  • Model.findByIdAndUpdate()
  • Model.findOne()
  • Model.findOneAndDelete()
  • Model.findOneAndRemove()
  • Model.findOneAndReplace()
  • Model.findOneAndUpdate()
  • Model.replaceOne()
  • Model.updateMany()
  • Model.updateOne()

They all return a query object.

Conclusion

We can use Mongoose query methods to query our database.