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.