Dictionary Attack in Express with Dynamodb
Dictionary Attack in Express with Dynamodb — how this specific combination creates or exposes the vulnerability
In an Express application that uses DynamoDB as its primary data store, a dictionary attack typically targets authentication or user enumeration endpoints. Attackers submit a large list of guessed usernames or email addresses to observe differences in response behavior, such as status codes or timing, to identify valid accounts. Because DynamoDB does not enforce built-in rate limiting or account lockout at the service level, these differences are more observable when the application layer does not normalize responses.
The combination of Express routing and DynamoDB access patterns can expose timing or logic vulnerabilities. For example, an endpoint like /api/login might query DynamoDB with a condition on a partition key (e.g., email = ?). If the application returns 404 Not Found for non-existent users and a generic 401 Unauthorized only when the password is wrong, an attacker can infer which emails exist in DynamoDB. DynamoDB’s predictable, low-latency responses for existing items versus potential throttling or delayed empty result sets for large scans can amplify timing differences.
Without protective measures such as consistent response times, rate limiting, or multi-factor authentication, dictionary attacks become practical. Attackers may use credential stuffing lists derived from prior breaches, leveraging automation to iterate through thousands of combinations. The unauthenticated attack surface that middleBrick scans tests increases risk: it checks Authentication and Rate Limiting in parallel with other controls, highlighting whether login endpoints leak account existence or allow excessive attempts.
Because DynamoDB stores data in a schema-less key-value structure, developers must enforce application-level protections. If Express routes directly expose DynamoDB query patterns without abstraction, attackers can probe behavior through crafted payloads. This is especially relevant when APIs use predictable identifiers (e.g., email as a partition key), making enumeration straightforward. MiddleBrick’s checks for BOLA/IDOR and Authentication emphasize whether sensitive operations rely on user-supplied identifiers without proper authorization checks.
Dynamodb-Specific Remediation in Express — concrete code fixes
To mitigate dictionary attacks in an Express service using DynamoDB, standardize responses, enforce rate limiting, and avoid leaking account existence. Below are concrete, realistic code examples that integrate the DynamoDB SDK with Express middleware.
1. Consistent response handling
Ensure login responses do not reveal whether an email exists. Use a fixed delay and identical HTTP status for invalid credentials.
const express = require('express');
const { DynamoDBClient, GetItemCommand } = require('@aws-sdk/client-dynamodb');
const { unmarshall } = require('@aws-sdk/util-dynamodb');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());
const ddb = new DynamoDBClient({ region: 'us-east-1' });
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const params = {
TableName: process.env.USERS_TABLE,
Key: { email: { S: email } }
};
try {
const data = await ddb.send(new GetItemCommand(params));
const user = data.Item ? unmarshall(data.Item) : null;
// Always perform a dummy hash to reduce timing differences
const dummyHash = await bcrypt.hash('dummy', 10);
if (user) {
await bcrypt.compare(password, user.passwordHash);
} else {
await bcrypt.compare(password, dummyHash);
}
// Always return the same generic message and status
res.status(401).json({ message: 'Invalid credentials' });
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Internal server error' });
}
});
2. Rate limiting and throttling at the application level
Use a token-bucket or sliding-window approach to limit requests per identifier or IP. MiddleBrick’s Rate Limiting check highlights endpoints that allow too many attempts.
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 50, // limit each IP to 50 requests per windowMs
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', apiLimiter);
3. Avoid direct exposure of DynamoDB keys in URLs
Do not use predictable identifiers in route parameters. If you must reference users, use opaque tokens and map them server-side.
app.get('/api/users/:userId/profile', async (req, res) => {
const { userId } = req.params;
// Validate and map userId to a DynamoDB key internally; avoid direct use
const params = {
TableName: 'Users',
Key: { id: { S: userId } }
};
// ... fetch and respond with normalized data
});
4. Use middleware to enforce MFA for sensitive actions
Add checks before critical operations to reduce the impact of compromised credentials.
function requireMfa(req, res, next) {
if (!req.user.mfaEnabled) {
return res.status(403).json({ message: 'MFA required' });
}
next();
}
app.post('/api/account/change-email', requireMfa, (req, res) => {
// proceed with change
});
5. Secure DynamoDB client configuration
Ensure environment variables define table names and region, avoiding hardcoded values that could lead to misconfiguration.
// server.js
require('dotenv').config();
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const client = new DynamoDBClient({
region: process.env.AWS_REGION,
});
module.exports = { client };