Insecure Design in Hanami with Dynamodb
Insecure Design in Hanami with Dynamodb — how this specific combination creates or exposes the vulnerability
Insecure design in a Hanami application that uses Amazon DynamoDB often centers on modeling decisions and authorization logic that do not enforce least privilege or validate tenant boundaries. DynamoDB’s key-based access patterns and flexible schema can encourage designs where a single table or index is shared across users, and application-level checks replace fine-grained controls. When combined with Hanami’s emphasis on explicit, domain-driven design, this can lead to BOLA/IDOR if object ownership is not enforced on every query.
For example, consider a multi-tenant Hanami service that stores user data in DynamoDB with a composite key like PK = USER#{user_id} and SK = METADATA. If the service constructs queries using only user input (e.g., an object ID from the URL) without verifying that the object’s PK matches the authenticated user’s ID, an attacker can manipulate the ID to access another user’s data. This is a classic BOLA (Broken Level Authorization) pattern, which OWASP API Top 10 lists under Broken Object Level Authorization. Because DynamoDB does not perform relational joins, the burden of verifying ownership falls entirely on the application; if Hanami’s repository or service objects omit this check, the design is insecure.
DynamoDB’s sparse index design can also contribute to insecure design. A common pattern uses a Global Secondary Index (GSI) to enable queries by non-key attributes such as status or tenant_id. If the GSI projection is broad and the application queries the GSI without validating tenant context, it may inadvertently expose data across tenants. For example, a query like Query(index_name: 'StatusIndex', key_condition_expression: 'status = :s') without a filter on tenant_id could return items belonging to other tenants. This becomes a data exposure issue when combined with insufficient encryption or overly permissive IAM policies on the DynamoDB table.
Insecure design also appears in how Hanami services consume DynamoDB output. If the domain model maps DynamoDB’s attribute-value format directly into rich objects without validating or sanitizing fields like URLs, paths, or S3 references, the application may be vulnerable to Server-Side Request Forgery (SSRF). An attacker could supply a malicious filename or URL in an item that the Hanami app later uses to make internal requests. Because DynamoDB stores data schemaless, the absence of strict input validation on write and read paths increases risk. The design must include explicit checks for parameter formats, safe URL resolution, and encryption at rest to meet secure design principles.
Finally, insufficient rate limiting and account enumeration patterns can stem from insecure design choices. DynamoDB’s provisioned capacity can throttle requests, but application-level controls must still exist. A Hanami endpoint that enumerates users via timing differences—such as returning a slightly different response time or status when a resource does not exist—can leak information. Secure design requires consistent response behavior and, where relevant, defensive queries that do not reveal existence. MiddleBrick’s LLM/AI Security checks can surface prompt injection or system prompt leakage in services that integrate AI features with DynamoDB-backed state, highlighting how insecure design can extend into emerging attack surfaces.
Dynamodb-Specific Remediation in Hanami — concrete code fixes
Remediation focuses on tightening authorization, validating inputs, and ensuring tenant-aware queries. In Hanami, encapsulate DynamoDB access in repositories that enforce ownership checks and use conditional expressions to prevent unauthorized updates. Below are concrete code examples for a Hanami domain Document with DynamoDB.
1. Enforce BOLA with partition-key ownership
Ensure every query includes the authenticated user’s ID in the key condition. This example uses the aws-sdk-dynamodb client within a Hanami repository:
require 'aws-sdk-dynamodb'
class DocumentRepository
def initialize(table_name = 'app_documents')
@dynamodb = Aws::DynamoDB::Client.new
@table = table_name
end
# Fetch a document only if it belongs to the user
def find_by_id(user_id, document_id)
pk = "USER##{user_id}"
sk = "DOCUMENT##{document_id}"
resp = @dynamodb.get_item(
table_name: @table,
key: {
'pk' => { s: pk },
'sk' => { s: sk }
}
)
item = resp.item
raise 'Not found' if item.nil?
to_domain(item)
end
private
def to_domain(item)
{ id: item['sk']['s'].sub('DOCUMENT##', ''), title: item['title']['s'] }
end
end
This design ensures that even if an attacker supplies a different document_id, the composed pk will not match another user’s partition key, preventing unauthorized access.
2. Secure GSI queries with tenant context
When using a GSI like OwnerStatusIndex (partition key tenant_id, sort key status), always include the tenant ID in the key condition and avoid scanning across tenants:
def list_documents_for_tenant(user_id, tenant_id, status)
index_name = 'OwnerStatusIndex'
key_condition_expression = 'tenant_id = :tid AND status = :st'
expression_attribute_values = {
':tid' => { s: "TENANT##{tenant_id}" },
':st' => { s: status }
}
filter_expression = 'user_id = :uid'
expression_attribute_values[':uid'] = { s: "USER##{user_id}" }
resp = @dynamodb.query(
table_name: @table,
index_name: index_name,
key_condition_expression: key_condition_expression,
filter_expression: filter_expression,
expression_attribute_values: expression_attribute_values
)
resp.items.map { |item| to_domain(item) }
end
Including user_id in the filter expression adds an extra authorization layer, and the tenant ID in the partition key prevents cross-tenant queries at the index level.
3. Validate inputs to mitigate SSRF and injection
Sanitize fields that could lead to SSRF (e.g., URLs stored in items) and validate formats before writing to DynamoDB:
def create_document(user_id, tenant_id, attrs)
url = attrs.fetch('source_url', '')
raise ArgumentError, 'Invalid URL' unless url.start_with?('https://')
item = {
pk: { s: "USER##{user_id}" },
sk: { s: "DOCUMENT##{SecureRandom.uuid}" },
tenant_id: { s: "TENANT##{tenant_id}" },
title: { s: attrs.fetch('title', '') },
source_url: { s: url },
created_at: { n: Time.now.to_i.to_s }
}
@dynamodb.put_item(table_name: @table, item: item)
to_domain(item)
end
Use conditional writes for critical operations to prevent race conditions, and enable encryption at rest via AWS KMS (configured in the table, not in Hanami code). For pricing tiers, the Free plan supports basic scans, while the Pro plan enables continuous monitoring and CI/CD integration to catch such design issues before deployment.