HIGH rainbow table attackflaskdynamodb

Rainbow Table Attack in Flask with Dynamodb

Rainbow Table Attack in Flask with Dynamodb — how this specific combination creates or exposes the vulnerability

A rainbow table attack leverages precomputed hashes to reverse cryptographic hashes such as unsalted SHA-256. In a Flask application using Amazon DynamoDB as the user store, the vulnerability arises when passwords (or other secrets) are stored as plain hashes without salting and when the API exposes behavior that aids an attacker, for example predictable user identifiers or endpoints that disclose whether a username exists.

Consider a typical Flask route that creates a user and stores a hash in DynamoDB:

import hashlib
import os
import boto3
from flask import Flask, request, jsonify

app = Flask(__name__)
ddb = boto3.resource('dynamodb', region_name='us-east-1')
table = ddb.Table('users')

def hash_password(password: str, salt: str = '') -> str:
    return hashlib.sha256((salt + password).encode()).hexdigest()

@app.route('/register', methods=['POST'])
def register():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    salt = os.urandom(16).hex()
    pwd_hash = hash_password(password, salt)
    table.put_item(Item={'username': username, 'pwd_hash': pwd_hash, 'salt': salt})
    return jsonify({'status': 'ok'}), 201

If the salt is stored but not used correctly during login, or if the implementation omits salt, the stored hashes may be vulnerable. More critically, if the login endpoint leaks information via timing differences or error messages, an attacker can enumerate valid usernames:

@app.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    response = table.get_item(Key={'username': username})
    item = response.get('Item')
    if not item:
        return jsonify({'error': 'invalid credentials'}), 401
    stored_hash = item['pwd_hash']
    salt = item.get('salt', '')
    attempt = hash_password(password, salt)
    if attempt == stored_hash:
        return jsonify({'status': 'ok'}), 200
    return jsonify({'error': 'invalid credentials'}), 401

An attacker can use a precomputed rainbow table (or generate one for common passwords) to map known hashes back to passwords. If your hashes are unsalted or use a low-entropy secret combined with a weak hash (e.g., unsalted SHA-256), a single leaked hash can lead to rapid recovery of the original password. In DynamoDB, if the table is improperly configured with overly permissive IAM policies, an attacker who gains read access can enumerate hashes at scale to feed offline cracking tools.

The combination of Flask routing that does not enforce rate limiting and DynamoDB access patterns that do not obscure valid usernames increases the risk. For example, an attacker can attempt logins for known usernames and observe response times or error messages to confirm valid accounts, then focus offline cracking efforts against the extracted hashes.

Dynamodb-Specific Remediation in Flask — concrete code fixes

Remediation focuses on proper password storage with per-user salts, safe comparison, and minimizing information leakage. Use a dedicated password hashing function rather than a plain cryptographic hash, and ensure DynamoDB access follows least privilege.

1) Use bcrypt or argon2 instead of SHA-256 for password hashing. Store the resulting hash (which already includes salt) in DynamoDB.

import bcrypt
import boto3
from flask import Flask, request, jsonify

app = Flask(__name__)
ddb = boto3.resource('dynamodb', region_name='us-east-1')
table = ddb.Table('users')

def hash_password(password: str) -> str:
    return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()

def verify_password(password: str, hashed: str) -> bool:
    return bcrypt.checkpw(password.encode(), hashed.encode())

@app.route('/register', methods=['POST'])
def register():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    pwd_hash = hash_password(password)
    table.put_item(Item={'username': username, 'pwd_hash': pwd_hash})
    return jsonify({'status': 'ok'}), 201

2) Ensure login performs a constant-time comparison by always running verification and returning a generic message, avoiding early exits based on username existence:

@app.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    response = table.get_item(Key={'username': username})
    item = response.get('Item')
    # Use a dummy hash to keep timing consistent when user not found
    dummy_hash = bcrypt.hashpw(b'dummy', bcrypt.gensalt()).decode()
    stored = item['pwd_hash'] if item else dummy_hash
    if verify_password(password, stored):
        # Only confirm username existence after a successful hash check
        # In practice, map session to a verified user record securely
        return jsonify({'status': 'ok'}), 200
    return jsonify({'error': 'invalid credentials'}), 401

3) Apply DynamoDB best practices: enable encryption at rest (default), use IAM policies that restrict the Flask role to dynamodb:GetItem and dynamodb:PutItem on the specific table, and avoid exposing raw hashes via other endpoints. If using the CLI to inspect findings, you can run:

middlebrick scan <your-api-url>

4) Consider adding rate limiting at the Flask layer (e.g., via Flask-Limiter) to reduce online guessing opportunities, and validate input to avoid injection or malformed requests that could affect DynamoDB operations.

Frequently Asked Questions

Why does storing unsalted SHA-256 hashes in DynamoDB increase rainbow table risk?
Unsalted hashes allow attackers to use precomputed rainbow tables to map hashes back to common passwords. Storing only a fast hash like SHA-256 also makes offline brute-force and dictionary attacks practical. Using a slow, salted hash function such as bcrypt or argon2 mitigates this by making each hash unique and computationally expensive to crack.
How does constant-time verification in the login flow reduce information leakage?
By always performing a hash comparison and returning a generic error regardless of whether the username exists, the login endpoint avoids timing differences and error-message clues that attackers can use to enumerate valid accounts. This prevents targeted offline attacks against specific users harvested from account enumeration.