Replay Attack in Feathersjs with Cockroachdb
Replay Attack in Feathersjs with Cockroachdb — how this specific combination creates or exposes the vulnerability
A replay attack occurs when an attacker intercepts a valid request and retransmits it to reproduce an authorized action. In a Feathersjs application backed by Cockroachdb, this risk is shaped by three dimensions: the framework’s default behavior, the database’s characteristics, and the absence of anti-replay controls at the API and data layers.
Feathersjs is a JavaScript framework that typically exposes REST or Socket.io endpoints. By default, Feathers does not enforce per-request uniqueness for mutating requests (POST/PUT/PATCH/DELETE). If an API route accepts a request like creating a payment or changing an email address, and the client does not include a nonce or idempotency key, an attacker can capture a valid authenticated request (e.g., via insecure network traffic or compromised logs) and replay it. Because Feathers routes often map directly to database records via an ORM/ODM layer, the same database transaction may be applied again unless the service or database prevents duplication.
Cockroachdb is a distributed SQL database that provides strong consistency and serializable isolation by default. While this prevents some classes of race conditions, it does not inherently prevent duplicate side effects caused by replayed writes. For example, if a Feathers service executes an INSERT without a uniqueness constraint on a business-level identifier (such as an idempotency key or a natural key like email), Cockroachdb will accept the duplicate row. This is because serializable isolation prevents anomalies like lost updates or write skews, but it does not automatically deduplicate identical writes that are syntactically valid. As a result, replayed requests can lead to double charges, duplicate records, or unintended state changes, despite Cockroachdb’s strong consistency guarantees.
The combination of Feathersjs’s permissive request handling and Cockroachdb’s acceptance of duplicate valid writes creates a practical replay surface. Attack vectors include: intercepted authentication tokens used to replay authenticated calls, reused nonces in signed requests, or missing idempotency tokens in financial operations. Even though Cockroachdb provides serializable transactions, the application must enforce uniqueness constraints and verify request intent. Without explicit checks, the system will faithfully execute each replay as a new operation, and findings from a middleBrick scan can highlight missing idempotency controls and weak authentication schemes that enable replay.
Cockroachdb-Specific Remediation in Feathersjs — concrete code fixes
To mitigate replay attacks in a Feathersjs service using Cockroachdb, implement uniqueness enforcement at the database level and idempotency at the API/service level. Below are concrete code examples that combine Feathers service hooks with Cockroachdb constraints and queries.
1. Database-level uniqueness constraint
Ensure critical fields have a uniqueness constraint in Cockroachdb. For example, to prevent duplicate emails in a users table, define the table with a unique index.
-- Cockroachdb SQL to create a users table with a unique email index
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email STRING NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
This constraint causes Cockroachdb to reject inserts with duplicate emails, providing a firm guarantee against replayed registration requests.
2. Idempotency key support in Feathers services
Use an idempotency key header (e.g., Idempotency-Key) and store processed keys in Cockroachdb to deduplicate requests. First, create an idempotency table:
-- Idempotency table in Cockroachdb
CREATE TABLE idempotency (
key STRING PRIMARY KEY,
user_id UUID NOT NULL,
response_payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Then, add a Feathers hook to check and record keys. This example uses a before hook for a payments service:
const { Client } = require('pg'); // or any Cockroachdb client used by Feathers
module.exports = function () {
const client = new Client({ connectionString: process.env.DATABASE_URL });
return async context => {
await client.connect();
const key = context.params.headers['idempotency-key'];
if (!key) {
throw new Error('Idempotency-Key header is required');
}
const query = 'INSERT INTO idempotency (key, user_id, response_payload) VALUES ($1, $2, $3) ON CONFLICT (key) DO NOTHING RETURNING key';
const result = await client.query(query, [key, context.params.user.id, '{}']);
await client.end();
if (!result.rows.length) {
// Key already processed — fetch and return the original response
const cached = await client.query('SELECT response_payload FROM idempotency WHERE key = $1', [key]);
context.result = JSON.parse(cached.rows[0].response_payload);
context.methodNotAllowed = true; // Signal to Feathers to skip further processing
} else {
// Continue with normal processing; result will be saved later in the hook chain
context.params.extensions = context.params.extensions || {};
context.params.extensions.idempotencyKey = key;
}
return context;
};
};
In your Feathers service, after the operation succeeds, store the response payload against the key. You can do this in a after hook or within the service logic, ensuring the response_payload column reflects the successful result. This ensures that replayed requests with the same key return the original response without re-executing side effects.
3. Conditional writes using unique business keys
If an idempotency header is not feasible, encode a business-level unique attribute into the write. For example, when creating a transaction record, include a unique client-generated transaction ID and use an upsert pattern:
-- Ensure a unique constraint on transaction_id
ALTER TABLE transactions ADD CONSTRAINT transactions_transaction_id_key UNIQUE (transaction_id);
// Feathers service create hook to upsert on transaction_id
module.exports = function (app) {
return async context => {
const tx = context.data;
const client = new (require('pg')).Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
const upsertQuery = `
INSERT INTO transactions (transaction_id, amount, user_id)
VALUES ($1, $2, $3)
ON CONFLICT (transaction_id) DO UPDATE SET processed = TRUE
RETURNING id, amount, processed;
`;
const result = await client.query(upsertQuery, [tx.transaction_id, tx.amount, tx.user_id]);
await client.end();
context.result = result.rows[0];
context.methodNotAllowed = result.rows[0].processed ? true : false;
return context;
};
};
By combining Cockroachdb’s unique constraints with Feathers hooks that validate idempotency keys or business keys, replayed requests are either deduplicated at the database level or safely short-circuited before causing duplicate side effects.