Insecure Direct Object Reference in Express with Dynamodb
Insecure Direct Object Reference in Express with Dynamodb — how this specific combination creates or exposes the vulnerability
Insecure Direct Object Reference (BOLA/IDOR) occurs when an API exposes internal object references such as database keys or IDs in requests and relies solely on client-supplied values to locate resources. In an Express service that uses DynamoDB as the persistence layer, this commonly maps to an endpoint like /users/:userId/profile where the route parameter userId is passed directly to a DynamoDB GetItem or Query without verifying that the requesting identity is authorized for that specific item.
Because DynamoDB does not enforce row-level ownership in the API layer, the onus is on the application to enforce authorization. If the Express route only checks that the userId exists in DynamoDB and returns the item, an attacker can iterate or manipulate identifiers (e.g., incrementing numeric IDs or guessing UUIDs) to access other users’ data. This becomes a BOLA/IDOR when authorization is missing or incomplete, even if the request is authenticated. MiddleBrick’s BOLA/IDOR checks flag this by correlating unauthenticated or low-privilege runtime calls with the OpenAPI definition to detect missing ownership checks on resource identifiers.
DynamoDB-specific exposures in Express often arise from these patterns:
- Using a client-provided key (e.g.,
userId,postId) directly inparams.Keywithout scoping to the requesting user or tenant. - Relying on client-supplied filter expressions or secondary indexes without validating that the requester is allowed to query that subset of data.
- Exposing predictable keys via URLs or client-side code, which enables enumeration and chaining with other endpoints.
For example, an Express route that forwards req.params.postId into a DynamoDB GetItem without ensuring the post belongs to the requesting user creates a straightforward IDOR. MiddleBrick’s unauthenticated scan surface may detect such endpoints by testing known IDs when no proper authorization context is enforced, highlighting the risk in the BOLA/IDOR category alongside remediation guidance mapped to frameworks such as OWASP API Top 10 and SOC2 controls.
Dynamodb-Specific Remediation in Express — concrete code fixes
Remediation centers on enforcing ownership or tenant boundaries on every DynamoDB access and avoiding direct exposure of internal keys in URLs. Below are concrete, executable patterns for Express routes using the AWS SDK for JavaScript v3.
1. Always scope queries to the authenticated subject. If your user identity is available on req.user (e.g., from a session or JWT), use it as a partition key or filter condition rather than trusting the client-supplied ID alone.
const { DynamoDBClient, GetItemCommand } = require("@aws-sdk/client-dynamodb");
const client = new DynamoDBClient({ region: "us-east-1" });
app.get("/users/:userId/profile", async (req, res) => {
// Enforce that the requesting user can only access their own profile
const requestingUserId = req.user.sub; // from auth context
const targetUserId = req.params.userId;
if (requestingUserId !== targetUserId) {
return res.status(403).json({ error: "Forbidden: cannot access other user’s profile" });
}
const command = new GetItemCommand({
TableName: process.env.USERS_TABLE,
Key: {
userId: { S: targetUserId }
}
});
try {
const { Item } = await client.send(command);
if (!Item) return res.status(404).json({ error: "Not found" });
res.json({ userId: Item.userId.S, email: Item.email.S });
} catch (err) {
res.status(500).json({ error: "Server error" });
}
});
2. Use a composite key design that includes the user identifier as part of the DynamoDB key to prevent cross-user access even if the ID is manipulated. For example, store userId#postId as the partition-sort key or enforce a filter expression that includes the user ID.
app.get("/users/:userId/posts/:postId", async (req, res) => {
const userId = req.user.sub;
const postId = req.params.postId;
const command = new GetItemCommand({
TableName: process.env.POSTS_TABLE,
Key: {
pk: { S: `USER#${userId}` },
sk: { S: `POST#${postId}` }
}
});
const { Item } = await client.send(command);
if (!Item) return res.status(404).json({ error: "Not found" });
res.json({ postId: Item.sk.S, content: Item.content.S });
});
3. When using queries or scans, include the user ID in the filter and avoid relying on client-supplied indexes for access control. For example, query posts by user and validate ownership on the server side.
const { DynamoDBClient, QueryCommand } = require("@aws-sdk/client-dynamodb");
const ddb = new DynamoDBClient({ region: "us-east-1" });
app.get("/users/:userId/posts", async (req, res) => {
const userId = req.user.sub;
const command = new QueryCommand({
TableName: process.env.POSTS_TABLE,
IndexName: "userId-index",
KeyConditionExpression: "userId = :uid",
ExpressionAttributeValues: {
":uid": { S: userId }
}
});
const { Items } = await ddb.send(command);
const posts = Items.map(i => ({ postId: i.sk.S, content: i.content.S }));
res.json(posts);
});
4. Avoid returning internal IDs directly to clients when possible, or transform them in a way that does not expose sequential or guessable values. If you must expose identifiers, consider hashing or namespacing them and maintain a server-side mapping in DynamoDB.
These patterns align with the checks MiddleBrick performs under its BOLA/IDOR and Property Authorization categories: validating that access patterns are scoped to the requester and that authorization is enforced at the data layer, not only at the endpoint.
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |