HIGH cache poisoningfeathersjscockroachdb

Cache Poisoning in Feathersjs with Cockroachdb

Cache Poisoning in Feathersjs with Cockroachdb — how this specific combination creates or exposes the vulnerability

Cache poisoning in a Feathersjs application backed by Cockroachdb arises when an attacker influences cached responses so that subsequent users receive malicious or incorrect data. Because Feathersjs often uses in-memory or external key-value caches and exposes REST or socket endpoints, cache poisoning can occur through unchecked input, missing key normalization, or unsafe query composition.

With Cockroachdb, the distributed SQL layer can return rows that differ per tenant, user context, or request parameters. If a cache key does not incorporate all dimensions that affect the query result — such as user ID, tenant ID, query filters, or locale — one user’s response may be served to another. For example, a query like SELECT * FROM invoices WHERE user_id = $1 cached only by the table name will incorrectly share data across users.

Feathersjs services typically define find and get methods that build dynamic queries. If these methods inject untrusted input directly into the query or into cache identifiers without canonicalization, an attacker can craft requests that cause cache entries to be overwritten. Techniques include path traversal in IDs, injection of special characters that change key hashing, or manipulation of pagination/sorting parameters that shift which rows are returned first.

The risk is compounded when Cockroachdb is used in a geo-distributed setup: reads may hit different nodes that have inconsistent cache states, and transaction isolation levels may not prevent a poisoned entry from propagating. Real-world patterns observed in OWASP API Top 10 A01:2023 (Broken Object Level Authorization) and A05:2021 (Security Misconfiguration) map to these scenarios, where improper access control and caching combine to expose or modify data across users.

An example attack chain includes: (1) User A requests /invoices?year=2023 and receives a response cached under key invoices:2023; (2) User B requests /invoices?year=2023&tenant=evil; (3) Due to missing tenant in the cache key, User A receives User B’s data; (4) If the poisoned cache contains sensitive fields, this constitutes a data exposure and potential privilege escalation.

Cockroachdb-Specific Remediation in Feathersjs — concrete code fixes

To remediate cache poisoning, normalize cache keys to include all context that affects the query result, and enforce strict input validation and authorization in Feathersjs services. Below are concrete steps and Cockroachdb examples tailored for Feathersjs.

1. Include tenant and user context in cache keys

Ensure cache keys incorporate tenant ID and user ID. In Feathersjs, customize the find method to build a deterministic key.

// src/services/invoices/invoices.class.js
const crypto = require('node:crypto');

class InvoicesService {
  constructor(options) {
    this.options = options;
  }

  async find(params) {
    const { user, query } = params;
    const tenantId = user.tenantId;
    const userId = user.id;
    const year = query.year || new Date().getFullYear();
    const sort = query.sort || 'createdAt';
    const direction = query.order === '1' ? 'asc' : 'desc';

    // Deterministic cache key that includes all context
    const cacheKey = crypto.createHash('sha256')
      .update(`invoices:tenant:${tenantId}:user:${userId}:year:${year}:sort:${sort}:dir:${direction}`)
      .digest('hex');

    // Check cache (example using a generic cache client)
    const cached = await this.cache.get(cacheKey);
    if (cached) return { data: JSON.parse(cached), total: cached.length };

    // Build query with explicit tenant filter and authorization
    const dbParams = {
      query: {
        tenantId,
        year,
        $sort: [[sort, direction]]
      },
      paginate: params.paginate
    };

    const result = await this.app.service('invoices-db').find(dbParams);

    // Store in cache with a reasonable TTL
    await this.cache.set(cacheKey, JSON.stringify(result.data), { ttl: 300 });

    return result;
  }
}

module.exports = function (app) {
  app.use('/invoices', new InvoicesService(app));
};

2. Use Cockroachdb placeholder binding and strict typing

When executing SQL against Cockroachdb, use parameterized queries to avoid injection that could alter cache behavior. In Feathersjs, you can integrate a Cockroachdb client via a hook or a custom service.

// src/hooks/cockroachdb-params.hook.js
module.exports = function cockroachdbParamsHook(options = {}) {
  return async context => {
    const { params, result } = context;
    if (context.method === 'find' && result && result.rows) {
      // Ensure columns referenced in sort are whitelisted
      const allowedSortColumns = ['created_at', 'updated_at', 'amount'];
      const sortBy = params.query.sortBy && allowedSortColumns.includes(params.query.sortBy)
        ? params.query.sortBy
        : 'created_at';
      const order = params.query.order === 'desc' ? 'DESC' : 'ASC';

      // Example of safe parameter binding for Cockroachdb
      const placeholderQuery = `SELECT * FROM invoices WHERE tenant_id = $1 AND extract(year FROM created_at) = $2 ORDER BY ${sortBy} ${order}`;
      const client = context.app.get('cockroachClient');
      const { rows } = await client.query(placeholderQuery, [params.user.tenantId, params.query.year]);
      context.result = { data: rows };
    }
    return context;
  };
};

3. Validate and sanitize inputs before caching

Feathersjs hooks should validate query parameters and reject suspicious values that could cause key divergence. Use a shared validation schema across services.

// src/validation/invoice-validation.js
const { isInt, isISO8601 } = require('validator');

function validateInvoiceQuery(query) {
  const errors = [];
  if (query.year && (!isInt(query.year) || Number(query.year) < 2000 || Number(query.year) > new Date().getFullYear() + 1)) {
    errors.push('Invalid year');
  }
  if (query.sortBy && !['created_at', 'updated_at', 'amount'].includes(query.sortBy)) {
    errors.push('Invalid sort column');
  }
  if (query.order && !['asc', 'desc'].includes(query.order)) {
    errors.push('Invalid order');
  }
  return errors;
}

// In a hook
module.exports = function validationHook(options) {
  return async context => {
    const errors = validateInvoiceQuery(context.params.query);
    if (errors.length) {
      throw new Error(`Validation failed: ${errors.join(', ')}`);
    }
    return context;
  };
};

4. Enforce row-level security at the service layer

Even with caching, ensure each request applies tenant and ownership checks. Do not rely solely on cache isolation.

// Example inline check within a Feathersjs service method
async ensureAccess(params) {
  const invoice = await this.app.service('invoices-db').get(params.id);
  if (!invoice || invoice.tenant_id !== params.user.tenantId) {
    throw new Error('Not found');
  }
  return invoice;
}

5. Configure cache TTL and eviction policies appropriately

Choose TTL values that balance freshness and load, and prefer explicit cache eviction when data changes. For Cockroachdb-backed services, listen to change streams or use timestamps to invalidate entries.

Frequently Asked Questions

How can I verify my cache keys include tenant and user context in Feathersjs?
Log the generated cache key in your Feathersjs service find method and confirm it incorporates fields like tenantId, userId, and all query parameters that affect the result. Unit tests that simulate requests for different tenants should produce distinct keys.
Does middleBrick detect cache poisoning risks in API scans?
middleBrick performs 12 security checks in parallel, including Property Authorization and Input Validation, which can surface misconfigurations that enable cache poisoning. Findings include severity, remediation guidance, and mapping to frameworks like OWASP API Top 10.