Skip to main content

Lack of protection against brute force attacks

Need

Implementation of strong authentication and rate limiting mechanisms

Context

  • Usage of TypeScript for type checking and static typing in Node.js development
  • Usage of Express for building web applications and APIs

Description

Non compliant code

import express from 'express';

const app = express();

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

// Perform authentication logic here

if (username === 'admin' && password === 'password') {
res.status(200).json({ message: 'Login successful' });
} else {
res.status(401).json({ message: 'Invalid credentials' });
}
});

app.listen(3000, () => {
console.log('Server is running on port 3000');
});

The vulnerability in this code is the lack of protection against brute force attacks. Brute force attacks involve automated attempts to guess credentials by trying different combinations of usernames and passwords until a successful match is found.

In this code, the /login endpoint accepts a POST request with a username and password in the request body. The code then performs a simple authentication check by comparing the provided username and password with the hardcoded values 'admin' and 'password'. If the credentials match, a successful login response is sent; otherwise, an invalid credentials response is sent.

The problem with this implementation is that there are no measures in place to prevent or detect brute force attacks. An attacker can repeatedly send requests to the /login endpoint with different username and password combinations until they find the correct credentials. This can be done using automated tools that can make thousands of login attempts in a short period of time.

To mitigate this vulnerability, it is recommended to implement controls that prevent or limit brute force attacks. Some common measures include:

  1. Implementing account lockout or rate limiting: After a certain number of failed login attempts, the account should be locked or the IP address should be temporarily blocked to prevent further login attempts.

  2. Implementing CAPTCHA: Adding a CAPTCHA challenge to the login form can help differentiate between human users and automated bots, making it harder for attackers to perform brute force attacks.

  3. Implementing strong password policies: Enforcing strong password requirements, such as minimum length, complexity, and expiration, can make it harder for attackers to guess passwords.

By implementing these controls, the system can better protect against brute force attacks and ensure that access is not granted to unauthorized users.

Steps

  • Implement rate limiting to limit the number of login attempts per IP address
  • Implement account lockout after a certain number of failed login attempts
  • Use strong and unique passwords for user accounts
  • Implement a CAPTCHA to prevent automated login attempts
  • Consider implementing two-factor authentication for added security
  • Regularly monitor and analyze login attempts for suspicious activity

Compliant code

import express from 'express';
import rateLimit from 'express-rate-limit';
import slowDown from 'express-slow-down';
import helmet from 'helmet';
import bcrypt from 'bcrypt';
import session from 'express-session';
import RedisStore from 'connect-redis';
import passport from 'passport';
import LocalStrategy from 'passport-local';

const app = express();
const RedisStore = RedisStore(session);

// Rate limiting middleware
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 requests per windowMs
});

// Slow down middleware
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000, // 15 minutes
delayAfter: 3, // allow 3 requests per windowMs, then start delaying
delayMs: 500, // delay each request by 500ms
});

// Helmet middleware for additional security headers
app.use(helmet());

// Session middleware
app.use(
session({
store: new RedisStore({ url: 'redis://localhost:6379' }),
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
})
);

// Passport middleware
app.use(passport.initialize());
app.use(passport.session());

// Local strategy for authentication
passport.use(
new LocalStrategy((username, password, done) => {
// Perform authentication logic here
// Replace the following code with your actual authentication logic
if (username === 'admin' && bcrypt.compareSync(password, hashedPassword)) {
return done(null, { id: 1, username: 'admin' });
} else {
return done(null, false, { message: 'Invalid credentials' });
}
})
);

// Serialize user object
passport.serializeUser((user, done) => {
done(null, user.id);
});

// Deserialize user object
passport.deserializeUser((id, done) => {
// Replace the following code with your actual user retrieval logic
const user = { id: 1, username: 'admin' };
done(null, user);
});

// Login route
app.post(
'/login',
limiter,
speedLimiter,
passport.authenticate('local', { failureRedirect: '/login' }),
(req, res) => {
res.status(200).json({ message: 'Login successful' });
}
);

app.listen(3000, () => {
console.log('Server is running on port 3000');
});

The fixed code addresses the lack of protection against brute force attacks by implementing several security measures.

  1. Rate limiting: The rateLimit middleware is used to limit the number of requests from a single IP address within a specified time window. In this case, it allows a maximum of 5 requests per IP address within a 15-minute window. This helps prevent automated brute force attacks by slowing down the rate at which login attempts can be made.

  2. Slow down: The slowDown middleware further slows down the rate of requests after a certain threshold is reached. In this case, it allows 3 requests per 15-minute window, and then introduces a delay of 500ms for each subsequent request. This adds an additional layer of protection against brute force attacks by making it even slower and more difficult for automated scripts to guess credentials.

  3. Helmet middleware: The helmet middleware is used to set various security headers in the HTTP response. These headers help protect against common web vulnerabilities, such as cross-site scripting (XSS) and clickjacking attacks.

  4. Session management: The code uses the express-session middleware to handle session management. Sessions are stored in a Redis store, which provides a more secure and scalable solution compared to in-memory storage. The session secret is set to a secure value, and options like resave and saveUninitialized are set to false to ensure that sessions are not unnecessarily saved or initialized.

  5. Authentication with Passport: The code uses the Passport library for authentication. It sets up a local strategy (passport-local) for username/password authentication. The actual authentication logic should be implemented in the callback function provided to the LocalStrategy constructor. In this example, a simple comparison is made between the provided username/password and a hardcoded admin username/password combination. However, this should be replaced with a proper authentication logic that securely validates user credentials.

  6. User serialization and deserialization: Passport provides methods to serialize and deserialize user objects. In this code, the serializeUser method is used to store the user's ID in the session, and the deserializeUser method is used to retrieve the user object based on the stored ID. The actual implementation should replace the hardcoded user object with a proper retrieval logic based on the user ID.

  7. Login route: The /login route is protected with the rate limiting and slow down middlewares. It uses the passport.authenticate method with the local strategy to handle the authentication process. If the authentication is successful, a JSON response with a "Login successful" message is sent.

Overall, the fixed code implements rate limiting, slow down, session management, secure headers, and proper authentication using Passport to address the vulnerability of lack of protection against brute force attacks.

References