Secure a Node.js Application
Implementing common security practices like preventing Cross-Site Request Forgery (CSRF) and securely storing user passwords are just a few ways to secure Node.js applications. Other strategies include enabling Cross-Origin Resource Sharing (CORS) for controlled cross-domain communication and creating secure connections using HTTPS/SSL/TLS.
HTTPS/SSL: Understanding Secure Connections
The HTTP protocol is layered on top of the SSL/TLS (Transport Layer Security/Secure Sockets Layer) protocol to enable secure communication. This is known as HTTPS (Hypertext Transfer Protocol Secure). Web applications need this encryption because it prevents unauthorized parties from viewing or altering data while it is in transit. HTTPS defaults to port 443 while HTTP normally uses port 80.
In order to create a simple HTTPS server in Node.js, you will typically require a public certificate and a private key. Using the openssl
commands, you can generate a self-signed certificate for development. Self-signed certificates should never be used in production settings, though, as they are not secure for such settings.
To create a self-signed certificate and development key, follow these steps:
openssl genrsa -out private-key.pem 1024 # Generates a private key
openssl req -new -key private-key.pem -out csr.pem # Creates a certificate signing request
openssl x509 -req -in csr.pem -signkey private-key.pem -out public-cert.pem # Signs the certificate request
To establish an HTTPS server in Node.js, you use the built-in https
module and specify your key and certificate files in an options
object. You can also configure the server to listen on a specific port, such as 4433.
Here’s an example of a basic HTTPS server:
const https = require('https'); // Loads the https module
const fs = require('fs'); // Loads the file system module to read certificate files
var httpsOptions = {
key: fs.readFileSync('path/to/server-key.pem'), // Path to your private key
cert: fs.readFileSync('path/to/server-cert.pem') // Path to your public certificate
};
var app = function (req, res) {
res.writeHead(200); // Set HTTP status code to 200 (OK)
res.end("hello world\n"); // Send "hello world" as the response
}
https.createServer(httpsOptions, app).listen(4433); // Create and listen on port 4433
console.log('HTTPS server listening on port 4433'); // Example output
You can designate distinct servers for each protocol, listening on distinct ports (e.g., 8888 for HTTP and 4433 for HTTPS) in order to accommodate both HTTP and HTTPS queries. A real SSL certificate should be bought from a provider for production. If a certificate authority (CA) chain is needed, it can be provided as an array to the ca
option.
CORS: Cross-Origin Resource Sharing
Cross-Origin Resource Sharing, or CORS, is a technique that permits a webpage to send requests to a domain other than the one that supplied the page. This is usually prohibited by web browsers because of same-origin policies. This is essential for contemporary web apps that frequently use APIs hosted on many sites.
CORS can be enabled in an Express.js application by configuring certain headers in your middleware. A common approach is to use a middleware function that sets the Access-Control-Allow-Origin
, Access-Control-Allow-Headers
, and Access-Control-Allow-Methods
headers on the response.
An illustration of how to enable CORS in Express.js that supports standard HTTP methods and permits requests from any origin (*
) is provided here:
const express = require('express'); // Import Express
const app = express(); // Create an Express application
app.use((req, res, next) => {
// Allow access from any origin ('*')
res.header('Access-Control-Allow-Origin', '*');
// Allow specific headers for preflight requests
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next(); // Pass control to the next middleware
// Handle preflight OPTIONS requests
app.options('*', (req, res) => {
// Allowed XHR methods
res.header('Access-Control-Allow-Methods', 'GET, PATCH, PUT, POST, DELETE, OPTIONS');
res.send(); // Send an empty response for preflight
});
});
// Example route
app.get('/api/data', (req, res) => {
res.json({ message: 'This is CORS-enabled data!' });
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`); // Example output
});
It’s a good idea to use a reverse proxy server (such as Apache or Nginx) to manage CORS in production and to selectively allow it, for example, solely in development settings. You may accomplish this by looking at process.env.NODE_ENV
.
Common Security Measures
Preventing Cross-Site Request Forgery (CSRF)
An end-user is forced to perform undesirable actions on a web application while they are currently authenticated in a cross-site request forgery (CSRF) attack. Browsers send cookies, including session cookies, with every request to a website, regardless of whether the request comes from another website. This makes the exploit viable.
Modules like csurf
can be used by Node.js apps to lessen the impact of CSRF attacks. Through the creation and validation of CSRF tokens, this module assists.
var express = require('express');
var cookieParser = require('cookie-parser'); // For cookie parsing
var csrf = require('csurf'); // CSRF module
var bodyParser = require('body-parser'); // For body parsing
// Setup route middlewares
var csrfProtection = csrf({ cookie: true }); // CSRF protection with cookie support
var parseForm = bodyParser.urlencoded({ extended: false }); // Body parser for URL-encoded forms
// Create express app
var app = express();
app.use(cookieParser()); // Parse cookies
app.get('/form', csrfProtection, function(req, res) {
// Generate and pass the csrfToken to the view
res.send(`
<form action="/process" method="POST">
<input type="hidden" name="_csrf" value="${req.csrfToken()}">
Name: <input type="text" name="name">
<button type="submit">Submit</button>
</form>
`);
});
app.post('/process', parseForm, csrfProtection, function(req, res) {
res.send('Data is being processed securely!');
});
const port = 3000;
app.listen(port, () => {
console.log(`Server listening on http://localhost:${port}`);
});
In order to verify that the request is authentic and not faked, the csurf
middleware verifies the CSRF token that the server creates when accessing /form
and inserts it into a hidden input field inside the form.
Securely Storing Passwords (Hashing with Bcrypt)
It is crucial for application security to securely store user passwords; instead of storing plain text passwords, they should always be hashed and salted before being saved to the database. Salting adds a random value to each password prior to hashing, which prevents attacks such as rainbow table lookups.
The bcryptjs
(or bcrypt-nodejs
) module is a widely recommended library for this purpose in Node.js.
Here’s how you can use bcryptjs
to hash and compare passwords:
const bcrypt = require('bcryptjs'); // Import bcryptjs
async function handlePassword() {
const plainPassword = 'MySuperSecurePassword123!';
const saltRounds = 8; // Recommended salt rounds for bcrypt
try {
// Hash the plain text password
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds); // Hashes the password
console.log('Hashed Password:', hashedPassword); // Output: Hashed Password: $2a$08$............................
// Later, when a user tries to log in, compare the provided password with the stored hash
const isMatch = await bcrypt.compare(plainPassword, hashedPassword); // Compares plain text to hash
console.log('Password Match:', isMatch); // Output: Password Match: true
const wrongPassword = 'WrongPassword';
const isWrongMatch = await bcrypt.compare(wrongPassword, hashedPassword);
console.log('Wrong Password Match:', isWrongMatch); // Output: Wrong Password Match: false
} catch (error) {
console.error('Error handling password:', error);
}
}
handlePassword();
To ensure that the actual user passwords are protected even in the event of a database compromise, the bcrypt.compare()
method securely determines whether a given plain text password matches a previously hashed password without re-hashing or disclosing the original. The bcrypt.hash()
method takes the plain text password and the number of salt rounds to generate a unique hash.