Introduction

Welcome to the "A04: Insecure Design" course! In this lesson, we will explore the topic of insecure credential recovery mechanisms, specifically focusing on the use of security questions and answers. Credential recovery is a critical component of web applications, allowing users to regain access to their accounts. However, when implemented insecurely, it can become a significant vulnerability.

Let's dive into understanding these mechanisms and how they can be exploited. 🚀

Historical Context of Security Questions

Security questions have been a popular authentication method since the early days of online banking and email services. The concept is simple: users choose predetermined questions and provide answers that only they should know. Common questions include "What's your mother's maiden name?", "What street did you grow up on?", or "What was your first pet's name?". While this method seemed secure initially, it has several fundamental flaws.

Other outdated recovery methods include:

  • SMS-based recovery (vulnerable to SIM swapping)
  • Simple PIN codes sent via email
  • Birth date verification
  • Last transaction amount (for banking)

The main security flaw with security questions is that the answers are often easily discoverable through social media, public records, or social engineering. This makes them particularly vulnerable to targeted attacks. Let's examine how these mechanisms typically work in code.

Understanding Credential Recovery Mechanisms

Credential recovery mechanisms are processes that allow users to regain access to their accounts when they forget their passwords. When implemented using security questions, the system compares user-provided answers against stored responses.

While this seems straightforward, it introduces several security risks because the answers are often predictable or publicly available information. Let's examine a vulnerable implementation that demonstrates these risks.

The Vulnerable Implementation

Let's examine a typical implementation of security question recovery, where the application first verifies the user exists and then checks their security answer. This is how the endpoint handles the initial recovery request:

router.post('/recover-password', async (req, res) => {
  const { username, securityAnswer } = req.body;
  const user = await User.findOne({ where: { username } });
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  // ...rest of the code follows
});

This code immediately confirms whether a username exists in the system. Although hiding user existence provides maximum security, many applications choose to reveal this information but protect against enumeration attacks using CAPTCHA or rate limiting - so there is a trade-off that's worth exploring here.

Next, let's see how it handles the security answer verification:

  if (securityAnswer === user.securityAnswer) {
    const tempPassword = Math.random().toString(36).slice(-8);
    const hashedPassword = await bcrypt.hash(tempPassword, 10);
    user.password = hashedPassword;
    await user.save();
    
    res.json({ newPassword: tempPassword });
  } else {
    res.status(401).json({ error: 'Incorrect security answer' });
  }

This implementation is particularly vulnerable because it performs a simple string comparison and immediately returns a temporary password. Additionally, it lacks rate limiting, account lockouts, and stores security answers in plaintext, all of which are significant security issues. Now, let's see how an attacker might exploit these vulnerabilities.

Exploiting the Vulnerability
A More Secure Approach: Email-Based Token System

Instead of relying on security questions, a more secure approach is to implement an email-based password reset system. This approach has become an industry standard used by major platforms like Google, Amazon, and Microsoft due to its robust security model.

The security of this method is multi-faceted and addresses several key vulnerabilities present in security question systems:

  • Email accounts serve as a trusted secondary authentication channel, often protected by two-factor authentication (though not all users enable 2FA)
  • Time-limited tokens ensure reset links expire quickly, typically within an hour
  • The system generates cryptographically secure tokens that are practically impossible to guess
  • Every reset attempt creates an audit trail through email and application logs

These security features work together to create a robust system that's both secure and user-friendly, while providing security teams the visibility they need to detect and respond to potential attacks.

Let's examine an example implementation of this secure solution step by step.

Implementing Secure Password Reset
Using Reset Tokens

The reset token mechanism is a crucial part of secure password recovery. The resetToken is typically a cryptographically secure random string that serves as a one-time password reset link. When stored in the database, it's associated with the user's account and given an expiration timestamp.

  const { token, tokenHash } = await generateResetToken();
  user.resetToken = tokenHash; // Store the hashed version
  user.resetTokenExpires = new Date(Date.now() + 3600000); // 1 hour
  await user.save();

  await sendPasswordResetEmail(user.email, token); // Send the raw token
  res.json({ message: 'Check your email for further instructions.' });

This implementation creates a secure, time-limited token and sends it via email. The token acts like a temporary key - it must be unique, unpredictable, and should expire after a short period (in this case, one hour) to minimize the window of opportunity for potential attacks.

Handling Password Reset Requests
Cryptographic Utilities for Secure Token Generation

To securely generate and store password reset tokens, use the following implementation:

import crypto from 'crypto';
import { promisify } from 'util';

const randomBytesAsync = promisify(crypto.randomBytes);

export async function generateResetToken() {
  const buffer = await randomBytesAsync(32);
  const token = buffer.toString('hex');
  const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
  return { token, tokenHash };
}

This implementation uses crypto.randomBytes() for generating unpredictable tokens and SHA-256 for secure token hashing, ensuring both the generation and storage of reset tokens are cryptographically secure. The token is sent to the user while the tokenHash is stored in the database, similar to how we handle passwords.

Note that SHA-256 is suitable for hashing tokens, but for passwords, a slow hash like bcrypt should be used.

Conclusion and Next Steps

In this lesson, we explored the vulnerabilities of using security questions for credential recovery and demonstrated how they can be exploited. We also discussed and implemented a secure email-based token system as a better alternative.

As you move forward, you'll have the opportunity to practice these concepts in the exercises that follow. In the next lesson, we'll continue to build on this foundation by exploring other common vulnerabilities and their mitigations. Keep up the great work! 🌟

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal