Guessable CAPTCHA

Description

Guessable CAPTCHA is a weakness where software uses a CAPTCHA challenge that can be guessed or automatically recognized by non-human actors. An effective CAPTCHA should be difficult for computers to solve while remaining easy for humans, but many implementations fail this requirement. Weaknesses include insufficient visual or audio distortion, use of recognizable formats like mathematical problems, limited answer sets (birth years, sports teams), general knowledge questions with answers available in databases, and metadata that reveals the CAPTCHA content (like filenames containing the answer). These flaws enable automated attackers to bypass the protection mechanism entirely.

Risk

When CAPTCHAs can be bypassed, automated attackers perform actions at rates far exceeding human capability. This enables spam campaigns, brute-force attacks against authentication, mass account creation, automated voting or review manipulation, and ticket scalping or inventory hoarding. Services that rely on CAPTCHAs to limit automated abuse become vulnerable to the exact attacks they intended to prevent. The false sense of security is particularly dangerous because organizations may not implement additional protective measures, believing the CAPTCHA provides adequate protection.

Solution

Use proven CAPTCHA services from reputable providers that employ machine learning to continually improve challenge difficulty. Implement strong distortion that makes optical character recognition difficult while remaining human-readable. Avoid using predictable formats like simple math problems, trivia questions, or limited answer pools. Ensure CAPTCHA implementation doesn't leak answers through metadata, predictable filenames, or weak hashing. Consider behavior-based alternatives like reCAPTCHA v3 that assess user behavior rather than explicit challenges. Implement rate limiting and other defenses as defense-in-depth measures rather than relying solely on CAPTCHAs. Regularly test CAPTCHA effectiveness against modern OCR and machine learning tools.

Common Consequences

ImpactDetails
Access ControlScope: Access Control

Bypass Protection Mechanism - Attackers can bypass CAPTCHA protection to perform automated actions at superhuman rates.
Access ControlScope: Access Control

Gain Privileges or Assume Identity - Automated systems can create fake accounts or bypass authentication attempts limits.
IntegrityScope: Integrity

Other - Mass manipulation of votes, reviews, or other user-generated content becomes possible.

Example Code

Vulnerable Code

# Vulnerable: CAPTCHA uses simple math that's easily parsed
import random

def vulnerable_generate_captcha():
    # Vulnerable: Simple math expressions are trivially solvable
    num1 = random.randint(1, 10)
    num2 = random.randint(1, 10)
    operator = random.choice(['+', '-', '*'])

    question = f"What is {num1} {operator} {num2}?"
    answer = eval(f"{num1}{operator}{num2}")

    return question, str(answer)

# Attacker can easily parse and solve: "What is 5 + 3?" -> 8
// Vulnerable: CAPTCHA answer revealed in filename
<?php
function vulnerable_generate_captcha() {
    $words = ['apple', 'banana', 'cherry', 'dog', 'elephant'];
    $word = $words[array_rand($words)];

    // Vulnerable: Answer is in the filename!
    $image_path = "/captcha/images/{$word}.png";

    return [
        'image' => $image_path,  // Attacker sees: /captcha/images/apple.png
        'answer' => $word
    ];
}
?>
// Vulnerable: Limited answer pool makes guessing easy
function vulnerableGenerateCaptcha() {
    // Vulnerable: Only 12 possible answers - 8.3% chance of correct guess
    const months = [
        'January', 'February', 'March', 'April',
        'May', 'June', 'July', 'August',
        'September', 'October', 'November', 'December'
    ];

    const question = "Select your birth month:";
    const correctMonth = months[Math.floor(Math.random() * 12)];

    return { question, answer: correctMonth, options: months };
}
# Vulnerable: Weak hash allows CAPTCHA prediction
import hashlib
import time

def vulnerable_generate_captcha():
    # Vulnerable: Predictable seed based on time
    timestamp = int(time.time())
    seed = hashlib.md5(str(timestamp).encode()).hexdigest()[:6]

    # Answer derived from predictable hash
    answer = seed.upper()

    return generate_image(answer), answer

# Attacker can predict timestamp and compute expected CAPTCHA
// Vulnerable: Trivia questions with searchable answers
public class VulnerableCaptcha {

    private static final String[][] TRIVIA = {
        {"What is the capital of France?", "Paris"},
        {"Who painted the Mona Lisa?", "Leonardo da Vinci"},
        {"What year did WWII end?", "1945"},
        // Vulnerable: All answers are easily searchable online
    };

    public String[] generateCaptcha() {
        int index = (int)(Math.random() * TRIVIA.length);
        return new String[]{TRIVIA[index][0], TRIVIA[index][1]};
    }
}
// Vulnerable: Insufficient visual distortion
void vulnerable_generate_captcha(char* text, Image* output) {
    // Generate random text
    generate_random_text(text, 6);

    // Vulnerable: No distortion - clean text easily OCR'd
    draw_text(output, text, "Arial", 20, BLACK);

    // No rotation, no noise, no overlapping lines
    save_image(output, "/tmp/captcha.png");
}

Fixed Code

# Fixed: Use reputable CAPTCHA service
import requests

def fixed_generate_captcha():
    # Fixed: Use Google reCAPTCHA or similar service
    # This returns a token that must be verified server-side
    return {
        'site_key': RECAPTCHA_SITE_KEY,
        'type': 'invisible'  # or 'checkbox' or 'v3'
    }

def fixed_verify_captcha(token, remote_ip):
    # Server-side verification
    response = requests.post(
        'https://www.google.com/recaptcha/api/siteverify',
        data={
            'secret': RECAPTCHA_SECRET_KEY,
            'response': token,
            'remoteip': remote_ip
        }
    )

    result = response.json()
    return result.get('success', False) and result.get('score', 0) >= 0.5
// Fixed: Strong visual distortion with random elements
<?php
function fixed_generate_captcha() {
    $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
    $answer = '';
    for ($i = 0; $i < 6; $i++) {
        $answer .= $chars[random_int(0, strlen($chars) - 1)];
    }

    // Create distorted image
    $image = imagecreatetruecolor(200, 80);
    $bg = imagecolorallocate($image, 255, 255, 255);
    imagefill($image, 0, 0, $bg);

    // Add noise lines
    for ($i = 0; $i < 10; $i++) {
        $color = imagecolorallocate($image, rand(100, 200), rand(100, 200), rand(100, 200));
        imageline($image, rand(0, 200), rand(0, 80), rand(0, 200), rand(0, 80), $color);
    }

    // Draw each character with random rotation and position
    for ($i = 0; $i < strlen($answer); $i++) {
        $color = imagecolorallocate($image, rand(0, 100), rand(0, 100), rand(0, 100));
        $angle = rand(-30, 30);
        $x = 20 + ($i * 28) + rand(-5, 5);
        $y = 50 + rand(-10, 10);
        imagettftext($image, rand(20, 28), $angle, $x, $y, $color, '/fonts/arial.ttf', $answer[$i]);
    }

    // Add noise dots
    for ($i = 0; $i < 500; $i++) {
        $color = imagecolorallocate($image, rand(100, 200), rand(100, 200), rand(100, 200));
        imagesetpixel($image, rand(0, 200), rand(0, 80), $color);
    }

    // Store answer in session, not filename
    $token = bin2hex(random_bytes(16));
    $_SESSION['captcha'][$token] = [
        'answer' => strtolower($answer),
        'expires' => time() + 300
    ];

    ob_start();
    imagepng($image);
    $data = ob_get_clean();
    imagedestroy($image);

    return [
        'token' => $token,
        'image' => 'data:image/png;base64,' . base64_encode($data)
    ];
}
?>
// Fixed: Behavior-based verification combined with challenge
const express = require('express');
const { RateLimiterMemory } = require('rate-limiter-flexible');

// Rate limiter as defense-in-depth
const rateLimiter = new RateLimiterMemory({
    points: 5,
    duration: 60
});

async function fixedVerifyCaptcha(req, res) {
    try {
        await rateLimiter.consume(req.ip);
    } catch (e) {
        return res.status(429).json({ error: 'Too many attempts' });
    }

    const { captchaToken, captchaAnswer, sessionToken } = req.body;

    // Verify server-stored answer
    const stored = captchaStore.get(sessionToken);
    if (!stored || stored.expires < Date.now()) {
        return res.status(400).json({ error: 'CAPTCHA expired' });
    }

    if (stored.answer.toLowerCase() !== captchaAnswer.toLowerCase()) {
        // Track failed attempts
        await recordFailedCaptcha(req.ip);
        return res.status(400).json({ error: 'Incorrect CAPTCHA' });
    }

    // Invalidate used CAPTCHA
    captchaStore.delete(sessionToken);

    return res.json({ success: true });
}
// Fixed: Use cryptographically secure generation with proper storage
import java.security.SecureRandom;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.concurrent.ConcurrentHashMap;

public class FixedCaptcha {
    private static final SecureRandom random = new SecureRandom();
    private static final String CHARS = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
    private static final ConcurrentHashMap<String, CaptchaData> store = new ConcurrentHashMap<>();

    public CaptchaResponse generateCaptcha() {
        // Generate random string
        StringBuilder answer = new StringBuilder();
        for (int i = 0; i < 6; i++) {
            answer.append(CHARS.charAt(random.nextInt(CHARS.length())));
        }

        // Create distorted image
        BufferedImage image = new BufferedImage(200, 80, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = image.createGraphics();

        // Background
        g2d.setColor(Color.WHITE);
        g2d.fillRect(0, 0, 200, 80);

        // Add distortion, noise, rotation (similar to PHP example)
        drawDistortedText(g2d, answer.toString());
        addNoise(g2d, image);

        // Generate secure token
        byte[] tokenBytes = new byte[16];
        random.nextBytes(tokenBytes);
        String token = bytesToHex(tokenBytes);

        // Store server-side with expiration
        store.put(token, new CaptchaData(
            answer.toString().toLowerCase(),
            System.currentTimeMillis() + 300000
        ));

        return new CaptchaResponse(token, imageToBase64(image));
    }

    public boolean verifyCaptcha(String token, String answer) {
        CaptchaData data = store.remove(token);  // Remove to prevent reuse
        if (data == null || data.expiresAt < System.currentTimeMillis()) {
            return false;
        }
        return data.answer.equalsIgnoreCase(answer);
    }
}

CVE Examples

  • CVE-2022-4036: An appointment booking application used weak hashing (CWE-328) for CAPTCHA generation, making the CAPTCHA answer predictable and easily guessable by attackers.

  • CWE-863: Incorrect Authorization (parent)
  • CWE-1390: Weak Authentication (parent)
  • CWE-330: Use of Insufficiently Random Values (can precede)
  • CWE-328: Use of Weak Hash (related)

References

  1. MITRE Corporation. "CWE-804: Guessable CAPTCHA." https://cwe.mitre.org/data/definitions/804.html
  2. OWASP. "Testing for CAPTCHA (WSTG-ATHN-08)." OWASP Web Security Testing Guide.
  3. Google. "reCAPTCHA Documentation." https://developers.google.com/recaptcha