Cache Poisoning in Loopback with Cockroachdb
Cache Poisoning in Loopback with Cockroachdb — how this specific combination creates or exposes the vulnerability
Cache poisoning in a Loopback application using CockroachDB occurs when an attacker manipulates cached responses so that malicious or incorrect data is served to users or downstream services. Because Loopback applications often cache query results to reduce database load and improve latency, and because CockroachDB supports distributed SQL with strong consistency guarantees, the interaction between caching logic and SQL execution can expose subtle risks.
Consider a typical Loopback model method that queries CockroachDB and caches the result:
const cache = new Map();
async function getUserById(userId) {
if (cache.has(userId)) {
return cache.get(userId);
}
const pool = new CockroachDBPool({ /* connection config */ });
const result = await pool.query('SELECT id, email, role FROM users WHERE id = $1', [userId]);
const row = result.rows[0];
cache.set(userId, row);
return row;
}
If the userId value is derived from user-controlled input without strict validation or normalization, an attacker can supply values that intentionally change the meaning of the cached key. For example, an input like 1 OR 1=1 could be transformed into a cache key that unexpectedly overlaps with legitimate numeric IDs if the application uses string coercion. More critically, if the caching layer is shared across tenants or users (for example, a global Map or a distributed cache like Redis without proper namespacing), one user’s poisoned key can affect others, leading to data leakage or incorrect privilege application.
Another vector involves query parameters that modify the SQL sent to CockroachDB but are still cached based on raw input. Suppose a Loopback filter is applied directly without sanitization:
async function listProducts(req) {
const { category, sort } = req.query;
const cacheKey = `products:${category}:${sort}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const pool = new CockroachDBPool({ /* connection config */ });
const query = `SELECT name, price FROM products WHERE category = $1 ORDER BY ${sort}`;
const result = await pool.query(query, [category]);
cache.set(cacheKey, result.rows);
return result.rows;
}
Here, sort is interpolated directly into the SQL string, and the resulting query is cached. An attacker could induce cache poisoning by submitting unusual sort expressions that change the execution plan or cause the cached result to be reused in an unsafe context. Because CockroachDB’s SQL engine optimizes and distributes queries across nodes, the poisoned cache entry may persist longer and propagate across nodes, increasing the blast radius.
In a distributed deployment, caching behavior can also interact with CockroachDB’s consistency settings. If a Loopback service reads with lower consistency (e.g., follower reads) and caches the response, it may serve stale or manipulated data until the cache expires. An attacker who can influence cache keys or eviction policies may force the system to retain poisoned entries, undermining the integrity guarantees that CockroachDB provides under normal conditions.
Cockroachdb-Specific Remediation in Loopback — concrete code fixes
To mitigate cache poisoning when using CockroachDB with Loopback, focus on strict input validation, deterministic cache key construction, and safe query composition. Below are concrete code examples that demonstrate secure patterns.
1. Validate and normalize all identifiers before caching
Ensure that numeric IDs are parsed as integers and that string-based keys are restricted to safe characters. Use a canonical representation for cache keys to avoid collisions:
function normalizeUserId(input) {
const num = Number(input);
if (!Number.isInteger(num) || num <= 0) {
throw new Error('Invalid user ID');
}
return num;
}
async function getUserById(userId) {
const id = normalizeUserId(userId);
const cacheKey = `user:profile:${id}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const pool = new CockroachDBPool({ /* connection config */ });
const result = await pool.query('SELECT id, email, role FROM users WHERE id = $1', [id]);
const row = result.rows[0];
if (!row) return null;
cache.set(cacheKey, row, { ttl: 30000 });
return row;
}
2. Parameterize sort and filter values; avoid direct interpolation
Never interpolate user input into SQL or cache keys. Instead, map allowed sort values to safe column names and use parameterized queries for data, while constructing cache keys from sanitized enumerations:
const ALLOWED_SORT = new Set(['price_asc', 'price_desc', 'name_asc']);
async function listProducts(req) {
const { category, sort } = req.query;
if (!ALLOWED_SORT.has(sort)) {
throw new Error('Invalid sort option');
}
const sortMap = {
price_asc: 'price ASC',
price_desc: 'price DESC',
name_asc: 'name ASC',
};
const cacheKey = `products:category:${category}:sort:${sort}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const pool = new CockroachDBPool({ /* connection config */ });
const query = 'SELECT name, price FROM products WHERE category = $1 ORDER BY ' + sortMap[sort];
const result = await pool.query(query, [category]);
cache.set(cacheKey, result.rows, { ttl: 60000 });
return result.rows;
}
3. Namespace caches and isolate tenant contexts
If your Loopback application serves multiple tenants or users from the same cache store, include tenant or user identifiers in the cache key and scope entries carefully to prevent cross-tenant poisoning:
async function getTenantData(tenantId, userId) {
const safeTenantId = tenantId.replace(/[^a-z0-9_-]/gi, '');
const safeUserId = normalizeUserId(userId);
const cacheKey = `tenant:${safeTenantId}:user:${safeUserId}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const pool = new CockroachDBPool({ /* connection config */ });
const result = await pool.query('SELECT data FROM tenant_data WHERE tenant_id = $1 AND user_id = $2', [safeTenantId, safeUserId]);
const payload = result.rows[0];
cache.set(cacheKey, payload, { ttl: 120000 });
return payload;
}
4. Configure appropriate cache TTLs and avoid caching sensitive or volatile data
Shorten TTLs for entries that may change frequently or that carry security implications. Avoid caching authentication or role information unless you can guarantee cache isolation and timely invalidation.
// Short TTL for sensitive profile data
cache.set(cacheKey, row, { ttl: 10000 });
By combining strict input validation, safe cache key design, and disciplined query handling, you reduce the risk of cache poisoning while retaining the performance benefits of caching with CockroachDB in Loopback.