Sandbox Escape in Adonisjs with Dynamodb
Sandbox Escape in Adonisjs with Dynamodb — how this specific combination creates or exposes the vulnerability
A sandbox escape in the AdonisJS + DynamoDB context occurs when user-controlled input influences how DynamoDB operations are constructed or authorized, allowing an attacker to bypass intended access boundaries. This typically maps to BOLA/IDOR and BFLA/Privilege Escalation checks in middleBrick scans, because the application may fail to enforce tenant or ownership checks before issuing DynamoDB requests.
AdonisJS does not inherently sandbox DynamoDB calls; it is the developer’s responsibility to ensure that every DynamoDB operation is scoped to the correct resource owner. A common pattern that leads to a sandbox escape is using a direct user-supplied identifier (e.g., userId or recordId) to build a DynamoDB key without verifying that the authenticated actor is allowed to access that identifier. For example, an endpoint that accepts a recordId query parameter and uses it as the partition key can expose records belonging to other users if the request is not first validated against the authenticated subject’s permissions.
DynamoDB itself does not interpret application-level permissions; it processes requests based on the credentials and policy provided by the caller. If AdonisJS generates requests using elevated or shared credentials, or omits ownership filters in the KeyConditionExpression, a compromised or malicious client can read or write items outside their scope. This becomes a sandbox escape when the attacker manipulates parameters to access or modify data that should be isolated, such as changing a numeric ID to enumerate other users’ records or leveraging secondary indexes to bypass intended query paths.
Additional risk arises from unvalidated input used in expression attribute names or values. If an endpoint dynamically builds expression attribute names from user input without strict allow-listing, an attacker may inject unexpected attribute references that expose or modify items beyond the intended scope. Because DynamoDB supports flexible schema structures, missing validation on nested attributes can further widen the blast radius of a poorly scoped query.
middleBrick detects these patterns by correlating unauthenticated request behavior with findings from the Authorization and Property Authorization checks, highlighting cases where operations lack proper scoping. The scanner does not assume trust in API structure and tests whether requests that appear unauthenticated can still elicit responses that reference other users’ data or administrative patterns.
Dynamodb-Specific Remediation in Adonisjs — concrete code fixes
Remediation centers on scoping every DynamoDB operation to the authenticated subject and strictly validating input before constructing requests. Always derive resource ownership from the authenticated session rather than from user-supplied identifiers alone. Use a two-step approach: (1) resolve the actor’s tenant or ownership context from the session, and (2) enforce that context in every DynamoDB key condition and filter expression.
Example: Safe record retrieval with ownership scoping
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext''
import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb'
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'
export default class RecordsController {
private readonly ddb = new DynamoDBClient({})
public async show({ params, auth }: HttpContextContract) {
const userId = auth.user?.id
const inputId = params.id
if (!userId) {
throw new Error('Unauthenticated')
}
// Enforce ownership before constructing the key
const candidate = await this.getRecord(inputId)
if (!candidate || candidate.userId !== userId) {
throw new Error('Not found')
}
return candidate
}
private async getRecord(id: string) {
const cmd = new GetItemCommand({
TableName: process.env.DYNAMO_TABLE,
Key: marshall({ id }),
})
const res = await this.ddb.send(cmd)
return res.Item ? unmarshall(res.Item) : null
}
}
Example: Query with explicit partition key and ownership filter
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { DynamoDBClient, QueryCommand } from '@aws-sdk/client-dynamodb'
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'
export default class ItemsController {
private readonly ddb = new DynamoDBClient({})
public async index({ request, auth }: HttpContextContract) {
const userId = auth.user?.id
if (!userId) {
throw new Error('Unauthenticated')
}
const status = request.input('status')
// Validate status against an allow-list to prevent injection
const allowedStatuses = ['active', 'pending', 'archived']
if (!allowedStatuses.includes(status)) {
throw new Error('Invalid status')
}
const cmd = new QueryCommand({
TableName: process.env.DYNAMO_TABLE,
KeyConditionExpression: 'userId = :uid AND status = :status',
ExpressionAttributeValues: marshall({
':uid': { S: userId },
':status': { S: status },
}),
})
const res = await this.ddb.send(cmd)
return res.Items?.map(unmarshall) ?? []
}
}
Input validation and allow-listing
Always validate identifiers and filter values against strict allow-lists or patterns before using them in DynamoDB expressions. Avoid concatenating user input into expression attribute names. If dynamic field access is necessary, use a controlled mapping instead.
// Unsafe: directly using user input in attribute name
// const attrName = request.input('field')
// const cmd = new UpdateItemCommand({
// TableName: TABLE,
// Key: marshall({ id }),
// UpdateExpression: `SET #field = :val`,
// ExpressionAttributeNames: { '#field': attrName },
// ExpressionAttributeValues: marshall({ ':val': { S: 'ok' } })
// })
// Safe: allow-listed mapping
const fieldMap: Record = {
status: 'status',
priority: 'priority',
tags: 'tags',
}
const fieldName = fieldMap[request.input('field')]
if (!fieldName) {
throw new Error('Invalid field')
}
Least-privilege credentials
Ensure the credentials used by AdonisJS for DynamoDB are scoped to the least privilege necessary. Prefer IAM policies that restrict actions and resources by ownership attribute (e.g., dynamodb:Query where the partition key must match the user’s ID). Avoid broad dynamodb:* permissions in production roles.
middleBrick’s Pro plan supports continuous monitoring and CI/CD integration to help catch regressions in permissions and scoping. By scanning on a configurable schedule and failing builds when risk scores drop below your threshold, you can prevent insecure changes from reaching production.