Insufficient Logging in Express with Dynamodb
Insufficient Logging in Express with Dynamodb — how this specific combination creates or exposes the vulnerability
Insufficient logging in an Express application that uses DynamoDB as a persistence layer creates blind spots that hinder detection, investigation, and response. Without structured, contextual logs for both application behavior and DynamoDB interactions, critical events go unrecorded or are recorded inconsistently.
Express, by default, does not log every request payload, headers, or outcome status in a way that maps cleanly to DynamoDB operations. When DynamoDB calls are made through the AWS SDK, developers often omit logging of request parameters (excluding sensitive data), condition checks, and error responses. This gap means authorization failures (such as IDOR or BOLA), malformed queries, or injection attempts are not captured with enough context to trace the originating request.
The combination is risky because DynamoDB’s schemaless design means application-level semantics (e.g., which attribute represents a tenant or user) are not enforced by the database. If Express does not log the interpreted tenant ID, the DynamoDB key condition expressions, or the raw query error codes, an incident response team cannot reliably determine whether a request targeted an unauthorized resource. Furthermore, insufficient logging of response metadata from DynamoDB (such as consumed capacity or throttling) can mask abuse patterns like rate-limited probing or data scraping.
Insecure default configurations may also contribute. For example, if DynamoDB client instrumentation is not explicitly enabled, SDK-level errors might surface only as generic 500 responses in Express, without a corresponding log line indicating the underlying DynamoDB ConditionalCheckFailedException or ProvisionedThroughputExceededException. The OWASP API Top 10 category ‘Security Logging and Monitoring Failures’ maps directly to this scenario, as does the PCI-DSS requirement to track access to cardholder data environments.
For LLM-related endpoints, insufficient logging becomes even more critical. Without logging of incoming prompts, tool call requests, and LLM output, an organization cannot detect prompt injection attempts, cost exploitation, or unexpected tool usage. middleBrick’s LLM/AI Security checks highlight this by testing for system prompt leakage and output anomalies; if your Express + DynamoDB stack does not log these interactions, you lose the ability to correlate suspicious LLM behavior with the underlying data access patterns.
Dynamodb-Specific Remediation in Express — concrete code fixes
Remediation centers on instrumenting Express routes and DynamoDB client calls to produce structured, actionable logs. Use a logging library to capture request identifiers, user context (when authenticated), query details, and SDK responses. Below are concrete patterns you can adopt.
Instrumenting Express with request-scoped logging
Add a request ID middleware to correlate logs across asynchronous DynamoDB calls:
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const logger = require('./logger'); // your structured logger
app.use((req, res, next) => {
req.id = uuidv4();
res.setHeader('X-Request-ID', req.id);
next();
});
Logging DynamoDB operations in Express routes
Log key details for each DynamoDB interaction, including parameters (excluding secrets), conditions, and outcomes:
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
app.get('/api/items/:id', async (req, res) => {
const requestId = req.id;
const userId = req.user ? req.user.sub : 'anonymous';
const { id } = req.params;
const params = {
TableName: process.env.DYNAMODB_TABLE,
Key: { id }
};
try {
const data = await dynamodb.get(params).promise();
if (!data.Item) {
logger.warn({
requestId,
userId,
operation: 'get',
table: params.TableName,
key: { id },
outcome: 'not_found'
});
return res.status(404).json({ error: 'Not found' });
}
logger.info({
requestId,
userId,
operation: 'get',
table: params.TableName,
key: { id },
outcome: 'success'
});
res.json(data.Item);
} catch (err) {
logger.error({
requestId,
userId,
operation: 'get',
table: params.TableName,
key: { id },
error: err.code,
message: err.message
});
res.status(500).json({ error: 'Internal server error' });
}
});
For write operations, include condition expression details to detect failed conditional updates, which are often indicative of concurrency issues or business logic violations:
app.put('/api/items/:id', async (req, res) => {
const requestId = req.id;
const userId = req.user ? req.user.sub : 'anonymous';
const { id } = req.params;
const { version, ...updateData } = req.body;
const params = {
TableName: process.env.DYNAMODB_TABLE,
Key: { id },
UpdateExpression: 'SET #data = :data, #version = :version',
ConditionExpression: '#version = :expectedVersion',
ExpressionAttributeNames: {
'#data': 'data',
'#version': 'version'
},
ExpressionAttributeValues: {
':data': updateData,
':version': version,
':expectedVersion': version
},
ReturnValues: 'ALL_NEW'
};
try {
const data = await dynamodb.update(params).promise();
logger.info({
requestId,
userId,
operation: 'update',
table: params.TableName,
key: { id },
condition: params.ConditionExpression,
outcome: 'success'
});
res.json(data.Attributes);
} catch (err) {
if (err.code === 'ConditionalCheckFailedException') {
logger.warn({
requestId,
userId,
operation: 'update',
table: params.TableName,
key: { id },
condition: params.ConditionExpression,
outcome: 'conditional_check_failed'
});
return res.status(409).json({ error: 'Conflict: item was modified' });
}
logger.error({
requestId,
userId,
operation: 'update',
table: params.TableName,
key: { id },
error: err.code,
message: err.message
});
res.status(500).json({ error: 'Internal server error' });
}
});
For queries and scans, log the filter criteria and consumed capacity to aid in cost and performance analysis:
app.get('/api/items', async (req, res) => {
const requestId = req.id;
const userId = req.user ? req.user.sub : 'anonymous';
const { status, limit = '10' } = req.query;
const params = {
TableName: process.env.DYNAMODB_TABLE,
IndexName: 'status-index',
KeyConditionExpression: 'status = :status',
ExpressionAttributeValues: { ':status': status },
Limit: parseInt(limit, 10)
};
try {
const data = await dynamodb.query(params).promise();
logger.info({
requestId,
userId,
operation: 'query',
table: params.TableName,
index: params.IndexName,
keyCondition: params.KeyConditionExpression,
consumedCapacity: data.ConsumedCapacity,
count: data.Count,
outcome: 'success'
});
res.json(data.Items);
} catch (err) {
logger.error({
requestId,
userId,
operation: 'query',
table: params.TableName,
index: params.IndexName,
keyCondition: params.KeyConditionExpression,
error: err.code,
message: err.message
});
res.status(500).json({ error: 'Internal server error' });
}
});
Ensure logs capture LLM-related interactions if your endpoints involve AI features: prompt inputs, tool call requests, and LLM outputs should be recorded with sufficient context to detect anomalies without storing prohibited data.