Building a Safer MERN App: Simple Steps for Strong Security
In today's digital era, small businesses are increasingly transitioning online, and web-based platforms offer significant advantages. As a result, developers often turn to the MERN stack (MongoDB, Express.js, React, Node.js) for building full-stack web applications. However, with the benefits of flexibility and ease of use come security concerns. This blog aims to provide a comprehensive guide to MERN stack security procedures, focusing on MongoDB, Express.js, React.js, and Node.js.
As with any website, both small and large are susceptible to security risks. The following sections will review some top MERN stack security checklists, which should help create safe online applications. Let's also identify its four components first.
MERN Technology Segments: A Closer Look
The MERN stack, comprising MongoDB, Express.js, React.js, and Node.js, empowers developers to create dynamic and scalable web applications using a unified JavaScript-based approach. In this closer look, we'll unravel the unique features and considerations associated with each component, shedding light on their individual roles in shaping the robust architecture of MERN applications.
MongoDB - The Data Powerhouse
MongoDB stands as a robust NoSQL database, tailored for managing massive unstructured data sets. Its capabilities in high scalability and availability make it an ideal choice for projects of varying sizes.
Express.js - Streamlining Web Development
Express.js, a web application framework, is utilized alongside Node.js to develop RESTful APIs. Recognized for its simplicity and efficiency, Express.js serves as a versatile middleware tool and API creator, making it well-suited for both hybrid and single-page web applications.
React.js - Building Dynamic User Interfaces
React.js, a front-end JavaScript library, is instrumental in creating reusable UI components. Its efficiency in developing complex user interfaces quickly, with minimal coding requirements, has positioned it as a cornerstone in modern web application development.
Node.js - The Backend Runtime
Node.js serves as an open-source runtime environment compatible with various operating systems, providing backend code execution. Its non-blocking I/O model and scalability make it an excellent choice for building expansive web applications.
Security best practices & suggestions
Compromized Database:
Strongly encrypt passwords with salt and hash (bcrypt): Storing plain-text passwords in a database is a significant security risk. Instead, passwords should be hashed using a strong cryptographic hash function. Bcrypt (Blowfish-crypt) is a widely recommended hash function for this purpose. Bcrypt incorporates a "salt" value, which is a random data unique to each password, hence making it much more difficult for attackers to use precomputed tables or rainbow tables to crack the hashed passwords.
const bcrypt = require('bcrypt'); const saltRounds = 10; const myPlaintextPassword = 's0/\/\P4$$w0rD'; const someOtherPlaintextPassword = 'youReading?'; bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) { // Store hash in your password DB. });
Strongly encrypt password reset tokens (SHA 256 Crypto): When a user requests a password reset, a unique token is often generated and sent to the user's email. This token is used to verify the user's identity during the password reset process. SHA-256 (Secure Hash Algorithm 256-bit) is a widely used cryptographic hash function. In the context of password reset tokens, it is used to create a fixed-size and unique hash value for each token. The use of SHA-256 ensures that the token's original value is not easily deducible from its hash, adding a layer of security against token manipulation.
Brute Force Attacks:
Use bcrypt to make login requests slow: Bcrypt, a robust password hashing algorithm, introduces intentional slowness into the hashing process. This deliberate slowing down significantly increases the time and computational resources required for each login attempt. As a result, brute-force attacks, which rely on rapid trial-and-error, become considerably more challenging and time-consuming for attackers.
Implement rate limiting using express-rate-limit: To counter rapid and successive login attempts, implement rate limiting. The express-rate-limit middleware in Node.js allows you to define a threshold for the number of requests a client can make within a specified time frame. If this threshold is exceeded, subsequent login requests are temporarily blocked, blocking brute force attempts.
const rateLimit = require('express-rate-limit'); // // const limiter = rateLimit({ max: 100, //max number of requests windowMs: 60 * 60 * 1000, //time in milliseconds (1hours) message: 'Too many requests from this IP, please try again in an hour' }) app.use('/api', limiter);
Implement maximum login attempts: Limit the number of consecutive login attempts a user can make. After reaching the specified maximum attempts, lock the account or introduce a delay before allowing additional login tries. This strategy discourages brute force attackers by slowing down their progress and adds an extra layer of protection against unauthorized access.
Cross-Site Scripting (XSS) Attacks:
Store JWT in HTTPOnly cookies: When dealing with JSON Web Tokens (JWT), storing them in HTTPOnly cookies is a recommended practice. HTTPOnly cookies restrict access to the cookie only through HTTP requests, preventing JavaScript from accessing them. This significantly mitigates the risk of XSS attacks, as malicious scripts executed on a user's browser won't be able to steal the JWT directly from the cookie.
const cookieOptions = { expires: new Date( Date.now() + process.env.JWT_COOKIE_EXPIRES_IN * 24 * 60 * 60 * 1000 ), httpOnly: true };
Sanitize user input data: Always validate and sanitize user input data on both the client and server sides. Sanitization involves filtering and cleaning input to remove potentially malicious code. 'xss-clean' package in Node.js implements this. It sanitizes user input from POST request bodies, GET request queries, and URL parameters. It does this by escaping characters that could be used to execute scripts, such as "<" and ">".
const xss = require('xss-clean'); // // app.use(xss());
Set special HTTP headers using helmet packag: Utilize the helmet package in Node.js to easily set various HTTP headers that enhance security. Specifically, the package helps in configuring headers like Content Security Policy (CSP), which instructs the browser to only execute scripts from trusted sources. This prevents the execution of any injected malicious scripts, a common vector for XSS attacks.
const helmet = require('helmet'); // // app.use(helmet()); //after this, check the response headers
Denial-of-Service (DoS) Attack:
Implement rate limiting using express-rate-limit: As discussed above, employing rate limiting, often facilitated by middleware like express-rate-limit in Node.js, helps control the number of requests from a single client within a specified time frame. By setting appropriate thresholds, you can mitigate the impact of DoS attacks by preventing a single source from overwhelming your server with an excessive number of requests. This throttling mechanism helps maintain service availability for legitimate users.
Limit body payload using in body-parser: In a DoS attack, attackers might send excessively large payloads to exhaust server resources. By using tools like body-parser with a configured payload limit, you can restrict the size of incoming request bodies. This prevents the server from being overloaded with massive data, contributing to resilience against DoS attacks.
const bodyParser = require('body-parser'); // // app.post( '/webhook-checkout', bodyParser.raw({ type: 'application/json' }), bookingController.webhookCheckout );
Avoid evil regular expressions: Crafting regular expressions with certain patterns can result in catastrophic backtracking, leading to significant performance degradation and potential DoS vulnerabilities. By avoiding complex and inefficient regular expressions, you reduce the risk of resource-intensive computations, enhancing your application's resilience against DoS attacks.
NoSQL Query Injection:
Use mongoose for MongoDB (because of Schema Types): Mongoose is an Object Data Modeling (ODM) library for MongoDB and provides a schema-based solution. When defining a schema in Mongoose, you specify the data types for each field. This helps prevent NoSQL injection by enforcing a structured format for data. Any attempt to inject malicious queries that don't adhere to the predefined schema will be rejected, providing a significant layer of security.
Sanitize user input data: As with other security measures, sanitizing user input is crucial. Validate and sanitize any input data before using it in a MongoDB query. This involves removing or escaping characters that could be exploited for injection attacks. By cleaning user input, you reduce the risk of an attacker injecting malicious queries into your NoSQL database.
const mongoSanitize = require('express-mongo-sanitize'); // // app.use(mongoSanitize()); //for sanitizing the data against NoSQL query injection
Other Suggestions:
Always use HTTPS
Create random password reset tokens with expiry dates
Deny access to JWT after password change
Don't commit sensitive config data to Git
Don't send error details to clients
Prevent Cross-Site Request Forgery (csurf package)
Require re-authentication before a high-value action
Implement a blacklist of untrusted JWT
Confirm user email address after first creating account
Keep user logged in with refresh tokens
Implement two-factor authentication
Prevent parameter pollution causing Uncaught Exceptions
Conclusion
In wrapping up, it's clear that in today's digital age, even small businesses going online can face security risks. This is where the MERN stack, with MongoDB, Express.js, React.js, and Node.js, steps in for developers to create powerful web apps. Yet, with all the benefits of flexibility and ease, security concerns arise.
The MERN stack is excellent for building top-notch web apps, but security is an ongoing task. Regular assessments and improvements are crucial to making sure our applications stay safe. By staying proactive and making regular improvements, we can protect our MERN stack applications from emerging threats and keep a strong defense against cyber risks.
Subscribe to my newsletter
Read articles from Divij Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Divij Sharma
Divij Sharma
Passionate about open source development and writing. SIH'23 Finalist, Codeforces specialist with 3 stars at CodeChef. Currently studying Computer Science at IIIT-J (Class of '26).