HIGH brute force attackexpress

Brute Force Attack in Express

How Brute Force Attack Manifests in Express

Brute force attacks in Express applications typically target authentication endpoints where attackers systematically try username/password combinations to gain unauthorized access. In Express, this often manifests at login routes like /login, /auth, or any custom authentication endpoint.

A common Express-specific pattern that enables brute force attacks is the use of asynchronous authentication middleware without rate limiting. Consider this vulnerable pattern:

app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  const user = await User.findOne({ username });
  
  if (user && await bcrypt.compare(password, user.password)) {
    req.session.user = user;
    res.json({ success: true });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

This endpoint is vulnerable because it provides no protection against rapid repeated attempts. An attacker can send thousands of requests per minute, each triggering a database query and bcrypt comparison. The bcrypt comparison itself becomes a performance bottleneck, as each failed attempt still requires computationally expensive password hashing.

Another Express-specific manifestation occurs with JWT token generation. Many developers implement token refresh endpoints without proper throttling:

app.post('/refresh', async (req, res) => {
  const token = req.cookies.refreshToken;
  
  if (token) {
    const payload = jwt.verify(token, process.env.REFRESH_SECRET);
    const user = await User.findById(payload.userId);
    
    if (user) {
      const newToken = generateJWT(user);
      res.json({ token: newToken });
    }
  }
  res.status(401).json({ error: 'Invalid token' });
});

Without rate limiting, this endpoint can be abused to cause denial of service through excessive JWT generation, consuming CPU resources and potentially database connections.

Express middleware order also creates specific vulnerabilities. If authentication middleware is applied before rate limiting middleware, the authentication logic executes before the rate check, wasting resources on every request:

// VULNERABLE ORDER
app.use(authenticateJWT);
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));

The correct order should be:

// SECURE ORDER
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
app.use(authenticateJWT);

Additionally, Express applications often expose admin interfaces or debug endpoints that lack authentication entirely, creating perfect targets for brute force attacks:

// DANGEROUS: admin endpoint without auth
app.get('/admin/stats', (req, res) => {
  res.json(getSystemStats());
});

Even if this endpoint doesn't contain sensitive data, it can be used as a measurement tool for timing attacks during brute force attempts against other endpoints.

Express-Specific Detection

Detecting brute force vulnerabilities in Express requires examining both the application code and runtime behavior. For code analysis, look for authentication endpoints that lack rate limiting middleware. middleBrick's Express-specific scanning identifies these patterns by analyzing the route structure and middleware chain.

When scanning an Express application with middleBrick, the tool examines your OpenAPI/Swagger spec (if available) and maps it to the actual runtime endpoints. For brute force detection, middleBrick specifically looks for:

  • POST endpoints with authentication-related paths (/login, /auth, /signin, etc.)
  • Endpoints that accept credentials without rate limiting
  • Password reset or recovery endpoints
  • Token refresh endpoints
  • Admin or debug endpoints without authentication

The scanner tests these endpoints by sending repeated requests and measuring response times and error patterns. A vulnerable endpoint will show consistent response times even under load, while a protected endpoint will demonstrate rate limiting behavior through HTTP 429 responses or increasing response delays.

For runtime detection in production Express apps, implement request logging middleware to track authentication failures:

const failedLogins = new Map();

app.use((req, res, next) => {
  if (req.path === '/login' && req.method === 'POST') {
    const ip = req.ip || req.connection.remoteAddress;
    const failures = failedLogins.get(ip) || 0;
    
    if (failures > 10) {
      console.warn(`Suspicious activity from ${ip}: ${failures} failed attempts`);
    }
  }
  next();
});

middleBrick's API security scanning complements this by providing a comprehensive assessment without requiring code changes. The scanner tests the actual attack surface that's exposed to the internet, catching configuration issues that code review might miss.

Another detection method involves analyzing error responses. Vulnerable Express endpoints often return detailed error messages that help attackers:

// VULNERABLE: detailed error messages
if (!user) {
  return res.status(401).json({ 
    error: 'User not found', 
    code: 'USER_NOT_FOUND' 
  });
}

if (!validPassword) {
  return res.status(401).json({ 
    error: 'Invalid password', 
    code: 'INVALID_PASSWORD' 
  });
}

middleBrick flags these patterns because they allow attackers to enumerate valid usernames before attempting password brute force.

Express-Specific Remediation

Express provides several native approaches for mitigating brute force attacks. The most effective is using the express-rate-limit middleware specifically on authentication endpoints:

const rateLimit = require('express-rate-limit');

const loginRateLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // limit each IP to 5 requests per windowMs
  message: {
    error: 'Too many login attempts, please try again later.'
  },
  standardHeaders: true,
  legacyHeaders: false,
});

app.post('/login', loginRateLimiter, async (req, res) => {
  // authentication logic
});

For more sophisticated protection, implement IP-based and account-based rate limiting:

const loginAttempts = new Map();

const checkBruteForce = async (req, res, next) => {
  const ip = req.ip;
  const { username } = req.body;
  
  const ipAttempts = loginAttempts.get(ip) || { count: 0, last: Date.now() };
  const accountAttempts = loginAttempts.get(username) || { count: 0, last: Date.now() };
  
  const now = Date.now();
  const fifteenMinutes = 15 * 60 * 1000;
  
  // Clean old attempts
  if (now - ipAttempts.last > fifteenMinutes) {
    loginAttempts.delete(ip);
  }
  if (now - accountAttempts.last > fifteenMinutes) {
    loginAttempts.delete(username);
  }
  
  if (ipAttempts.count > 10 || accountAttempts.count > 10) {
    return res.status(429).json({ 
      error: 'Too many attempts, try again later' 
    });
  }
  
  next();
};

app.post('/login', checkBruteForce, async (req, res) => {
  const { username } = req.body;
  const ip = req.ip;
  
  // Increment counters
  loginAttempts.set(ip, { 
    count: (loginAttempts.get(ip)?.count || 0) + 1, 
    last: Date.now() 
  });
  
  loginAttempts.set(username, { 
    count: (loginAttempts.get(username)?.count || 0) + 1, 
    last: Date.now() 
  });
  
  // authentication logic
});

Another Express-specific mitigation is using express-slow-down for progressive delays:

const slowDown = require('express-slow-down');

const loginSlowDown = slowDown({
  windowMs: 15 * 60 * 1000,
  delayAfter: 3,
  delayMs: 500, // start with 500ms delay
});

app.post('/login', loginSlowDown, loginRateLimiter, async (req, res) => {
  // authentication logic
});

This approach makes brute force attacks impractical by exponentially increasing response times after multiple failures.

For distributed applications, use Redis-backed rate limiting:

const Redis = require('ioredis');
const RateLimit = require('express-rate-limit');

const redis = new Redis(process.env.REDIS_URL);

const limiter = new RateLimit({
  store: new RateLimit.RedisStore({ redis }),
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: 'Too many requests from this IP'
});

Finally, implement proper error handling that doesn't reveal whether a username exists:

app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  let shouldLogin = false;
  let user;
  
  try {
    user = await User.findOne({ username });
    if (user) {
      shouldLogin = await bcrypt.compare(password, user.password);
    }
  } catch (err) {
    console.error(err);
  }
  
  if (shouldLogin) {
    req.session.user = user;
    return res.json({ success: true });
  }
  
  // Always return the same response regardless of failure reason
  res.status(401).json({ 
    error: 'Invalid credentials' 
  });
});

This prevents username enumeration, forcing attackers to brute force both username and password simultaneously.

Frequently Asked Questions

How does middleBrick detect brute force vulnerabilities in Express applications?
middleBrick scans the unauthenticated attack surface by testing authentication endpoints with rapid repeated requests. It analyzes your OpenAPI spec to identify login, auth, and password reset endpoints, then measures response consistency and error patterns. The scanner looks for endpoints that lack rate limiting, return detailed error messages, or show no throttling behavior under load. middleBrick provides a security risk score with specific findings about brute force vulnerabilities and remediation guidance.
Can I integrate brute force protection into my Express CI/CD pipeline?
Yes, using middleBrick's GitHub Action you can automatically scan your Express APIs during CI/CD. Add middleBrick to your workflow to scan staging APIs before deployment, with configurable thresholds that fail builds if security scores drop below your requirements. The Pro plan includes continuous monitoring that scans your production APIs on a schedule and sends alerts if brute force vulnerabilities are detected, helping you maintain security posture over time.