Sandbox Escape in Django with Dynamodb
Sandbox Escape in Django with Dynamodb — how this specific combination creates or exposes the vulnerability
In Django applications that use Amazon DynamoDB as a persistence layer, a sandbox escape can occur when untrusted input influences how DynamoDB requests are constructed or authorized. Unlike SQL databases, DynamoDB relies on permissions scoped to AWS resources (tables, indexes, streams) and uses condition expressions and partition/sort key patterns for access control. If Django does not strictly validate or isolate tenant or user input used to build table names, key condition expressions, or filter criteria, an attacker may pivot across logical boundaries that the application intends to enforce.
For example, consider a multi-tenant Django service that stores tenant data in a single DynamoDB table using a tenant_id attribute as part of the partition key. If the tenant identifier is derived from user input (e.g., subdomain or header) without strict allowlisting, an attacker may attempt to inject a specially crafted value such as tenant_id = "abc OR 1=1" or exploit path traversal patterns to reference a different logical partition. While DynamoDB does not support SQL-style injection, the misuse of key schema and expression attribute values can lead to unauthorized reads or writes across tenant boundaries, effectively a sandbox escape within the application’s security model.
Another vector involves DynamoDB’s support for expression attribute names and values. If Django code dynamically constructs ExpressionAttributeNames or ExpressionAttributeValues using unsanitized input, an attacker may supply keys such as __attribute__ or reserved words that alter the semantics of the condition. In combination with broader IAM permissions assigned to the application’s execution role, this can enable privilege escalation or data exposure beyond the intended scope, mapping to BOLA/IDOR and Property Authorization findings in middleBrick’s security checks.
Because DynamoDB is often perceived as a managed, isolated service, developers may assume that network or service boundaries alone provide sufficient sandboxing. However, in Django, improper request validation and insufficient schema design can allow logical escapes across what appear to be segregated data domains. This is particularly relevant when using the AWS SDK for Python (Boto3) directly within Django models or services without an abstraction layer that enforces tenant isolation and input canonicalization.
When scanning such an API with middleBrick, checks related to Authentication, BOLA/IDOR, and Property Authorization will surface misconfigurations that may permit boundary violations across logical sandboxes. The scanner does not modify behavior but highlights how untrusted input can affect DynamoDB expression construction and how per-category findings align with compliance frameworks like OWASP API Top 10 and SOC2. Understanding these mappings helps developers implement concrete remediation in the Django and DynamoDB integration layer.
Dynamodb-Specific Remediation in Django — concrete code fixes
Remediation focuses on strict input validation, canonical key construction, and safe use of DynamoDB expression syntax. The following examples demonstrate secure patterns for common Django integration patterns using Boto3.
1. Enforce tenant isolation with allowlist and canonical keys
Validate tenant identifiers against a known allowlist and derive DynamoDB keys using a deterministic mapping rather than raw user input.
import boto3
from django.conf import settings
ALLOWED_TENANTS = {"acme", "globex", "initech"}
def get_dynamodb_table():
# Use a single table design with tenant_id as partition key attribute
return boto3.resource("dynamodb", region_name=settings.AWS_REGION).Table("AppTable")
def safe_get_tenant_data(tenant_slug: str, item_key: str):
if tenant_slug not in ALLOWED_TENANTS:
raise ValueError("Invalid tenant")
# Canonicalize tenant_id; avoid concatenation that could lead to injection
tenant_id = f"tenant_{tenant_slug}"
table = get_dynamodb_table()
response = table.get_item(
Key={"tenant_id": tenant_id, "entity_id": item_key}
)
return response.get("Item")
2. Use static expression attribute names for dynamic field access
When filtering or updating based on user-provided field identifiers, map them to static expression attribute names to prevent key manipulation.
def safe_query_with_field(tenant_slug: str, entity_id: str, sort_key: str, allowed_sort_keys: set):
if sort_key not in allowed_sort_keys:
raise ValueError("Invalid sort key")
tenant_id = f"tenant_{tenant_slug}"
table = get_dynamodb_table()
# Map user input to a safe attribute name
field_expr_attr_name = "#sort"
response = table.query(
KeyConditionExpression="tenant_id = :tid AND #sort = :val",
ExpressionAttributeNames={"#sort": sort_key},
ExpressionAttributeValues={":tid": tenant_id, ":val": "active"}
)
return response["Items"]
3. Avoid constructing expression attributes from concatenated strings
Never build condition expressions by interpolating raw values. Use ExpressionAttributeValues for all data inputs.
def safe_update_with_validation(tenant_slug: str, entity_id: str, update_key: str, update_value):
if update_key not in {"status", "priority", "metadata"}:
raise ValueError("Invalid update key")
tenant_id = f"tenant_{tenant_slug}"
table = get_dynamodb_table()
table.update_item(
Key={"tenant_id": tenant_id, "entity_id": entity_id},
UpdateExpression="SET #field = :val",
ConditionExpression="attribute_exists(entity_id)",
ExpressionAttributeNames={"#field": update_key},
ExpressionAttributeValues={":val": update_value}
)
4. Scope IAM permissions tightly at the application level
Even when using middleBrick to detect overly permissive IAM roles, enforce least privilege in your Django deployment. Configure the AWS credential used by Boto3 to allow only the required table actions on the specific table prefix for the tenant scope.
5. Validate and normalize inputs before use in key expressions
Normalize casing, trim whitespace, and reject control characters to reduce edge cases that could enable bypass patterns.
def normalize_and_validate_input(raw: str) -> str:
normalized = raw.strip().lower()
if not normalized.isalnum() or ".." in normalized or "/" in normalized:
raise ValueError("Invalid input")
return normalized