Cache Poisoning in Sails with Dynamodb
Cache Poisoning in Sails with Dynamodb — how this specific combination creates or exposes the vulnerability
Cache poisoning in a Sails application that uses DynamoDB as a persistent or caching data store occurs when an attacker causes incorrect or malicious data to be written into the cache layer and subsequently served to other users or to the same user in different contexts. This typically maps to the OWASP API Top 10:2023 – Injection and Broken Object Level Authorization (BOLA) categories, especially when cache-control logic is coupled to DynamoDB query parameters or item keys.
In Sails, which follows a model-view-controller pattern, cache-related behavior often lives in policies or services that decide whether a request should be served from cache or should hit DynamoDB. If these decisions are based solely on user-supplied input—such as query parameters, path segments, or headers—without strict validation and canonicalization, an attacker can manipulate that input to change the cache key. A manipulated key may result in cache misses that forward the request to DynamoDB, or worse, write attacker-controlled values into the cache under keys that other users will later retrieve.
DynamoDB itself does not provide native cache poisoning protections; it stores and returns exactly what the application writes. Therefore, if Sails writes responses into DynamoDB based on tainted inputs, those tainted responses can persist and be reused. For example, an item ID derived from a URL parameter could allow an attacker to force writes to a shared item key, overwriting data that other users or services expect to see. Additionally, if Sails stores rendered API responses or authorization contexts in DynamoDB and uses unvalidated identifiers as keys, the poisoned cache can lead to privilege escalation or data leakage across users.
Because middleBrick tests unauthenticated attack surfaces and includes checks such as BOLA/IDOR and Input Validation, it can surface cache poisoning risks in this configuration by identifying weak key derivation, missing integrity checks, and insufficient authorization on data retrieval paths. The scanner also checks for excessive agency patterns and unsafe consumption, which can be relevant if Lambda functions or other automation write to DynamoDB on behalf of cached results.
Dynamodb-Specific Remediation in Sails — concrete code fixes
To mitigate cache poisoning when using DynamoDB with Sails, enforce strict input validation, canonicalize cache keys, and isolate data by tenant or user context. Below are concrete steps and code examples.
- Validate and sanitize all inputs that influence cache keys or DynamoDB item identifiers. Use a validation library (e.g.,
joi) to enforce type, length, and pattern constraints.
// api/validators/cacheKey.js
'use strict';
const Joi = require('joi');
module.exports = {\n friendlyName: 'Validate cache key',
description: 'Ensure cache keys are safe and canonical.',
inputs: {
key: { type: 'string', required: true },
userId: { type: 'string', required: true }
},
exits: {
invalid: { statusCode: 400 },
ok: { },
},
fn: function (inputs, exits) {
const schema = Joi.object({
key: Joi.string().pattern(/^[a-zA-Z0-9\-_]+$/).max(128).required(),
userId: Joi.string().pattern(/^[a-zA-Z0-9\-]+$/).required()
});
const result = schema.validate(inputs);
if (result.error) { return exits.invalid(); }
// Canonical key format: userId:resourceType:normalizedKey
const canonical = `${inputs.userId}:item:${result.value.key}`;
return exits.ok({ canonical });
}
};
- Use canonical keys that incorporate user or tenant context to prevent key collisions and cross-user reads/writes. When reading from or writing to DynamoDB, derive keys from validated inputs combined with a user or tenant identifier.
// api/services/dynamo-cache.js
'use strict';
const AWS = require('aws-sdk');
const docClient = new AWS.DynamoDB.DocumentClient({ region: 'us-east-1' });
module.exports = {
async getOrCreate(key, userId, fetchFromSource) {
const { canonical } = await sails.helpers.validateCacheKey({ key, userId });
const params = {
TableName: 'AppCache',
Key: { pk: canonical }
};
const cached = await docClient.get(params).promise();
if (cached.Item && cached.Item.value != null) {
return cached.Item.value;
}
const fresh = await fetchFromSource();
await docClient.put({
TableName: 'AppCache',
Item: {
pk: canonical,
value: fresh,
ttl: Math.floor(Date.now() / 1000) + 300 // 5 minutes
}
}).promise();
return fresh;
}
};
- Enforce authorization on every DynamoDB operation by re-checking the relationship between the requesting user and the item’s owning user or tenant, rather than relying on cache presence alone.
// api/actions/get-user-item.js
module.exports = {
friendlyName: 'Get user item with cache check',
description: 'Retrieve an item from cache or DynamoDB with ownership verification.',
inputs: {
itemId: { type: 'string', required: true },
requesterId: { type: 'string', required: true }
},
exits: {
notFound: { statusCode: 404 },
forbidden: { statusCode: 403 },
ok: { },
},
fn: async function (inputs, exits) {
const canonical = `${inputs.requesterId}:item:${inputs.itemId}`;
const cached = await sails.helpers.getCacheEntry(canonical, inputs.requesterId);
if (cached) { return exits.ok(cached); }
// Direct DynamoDB fetch with ownership check
const params = {
TableName: 'UserItems',
Key: { userId: inputs.requesterId, itemId: inputs.itemId }
};
const result = await docClient.get(params).promise();
if (!result.Item) { return exits.notFound(); }
// Ensure the item belongs to the requester (redundant check)
if (result.Item.ownerId !== inputs.requesterId) { return exits.forbidden(); }
await sails.helpers.cacheEntry(canonical, result.Item);
return exits.ok(result.Item);
}
};
- Configure DynamoDB TTL to automatically expire stale entries and reduce the window for poisoned cache persistence.
By combining strict input validation, canonical and isolated keys, and re-verifying ownership on each DynamoDB access, Sails applications can avoid cache poisoning while still benefiting from performance gains.