Insufficient Logging in Feathersjs with Firestore
Insufficient Logging in Feathersjs with Firestore — how this specific combination creates or exposes the vulnerability
Insufficient logging in a Feathersjs service that uses Cloud Firestore means that important events—such as create, update, patch, and remove—are not recorded with enough context to support investigation, audit, or detection. When Firestore is used as the backing store, operations are typically asynchronous and permission checks are often handled via Firestore security rules rather than explicit application logic, which can make it harder to correlate requests with outcomes if logging is incomplete or unstructured.
In Feathers, services define behavior through hooks and service methods. If a developer does not explicitly log key inputs, identities (or the absence of them), and the resulting Firestore write IDs or document paths, important signals are lost. For example, a patch request that updates a sensitive field might only produce a minimal console message or none at all, while the Firestore document silently reflects the change. Without structured logs that include timestamps, user or client context (when identifiable), operation type, document ID, and before/after summaries, defenders cannot reliably detect patterns such as abnormal update frequencies, suspicious field changes, or unauthorized data access.
This gap is especially relevant for the LLM/AI Security checks in middleBrick, where system prompt leakage or output containing secrets can indicate that sensitive behavior is not being captured and retained for review. Similarly, findings related to Authentication, BOLA/IDOR, Data Exposure, and Unsafe Consumption highlight the need for detailed logs to trace how requests were authorized and what data was affected. In environments that rely on Firestore’s rule-based model, logging at the Feathers layer becomes the primary source of verifiable audit trails, because Firestore server logs are typically outside the direct control of the application and may not provide the required level of operational context.
Concrete risks enabled by insufficient logging include delayed incident response, inability to reconstruct chains of events for forensics, and challenges in demonstrating compliance with frameworks such as OWASP API Top 10, SOC2, and GDPR. For instance, without logs that record which fields changed during a patch operation and who initiated it, an organization may struggle to determine whether a BOLA/IDOR incident involved privilege escalation or data exfiltration. middleBrick scans can surface these logging gaps by correlating runtime behavior with expected audit signals, emphasizing the need for structured, actionable log entries that capture method, path, identity context, and outcome in Feathers services backed by Firestore.
Firestore-Specific Remediation in Feathersjs — concrete code fixes
To address insufficient logging when using Feathersjs with Firestore, add explicit, structured logging inside service hooks and around Firestore operations. Ensure logs include timestamp, operation type, document ID, user context (when available), and a safe summary of changes. Avoid logging sensitive fields in full, and structure logs as JSON to simplify ingestion by monitoring tools.
Below are Firestore-integrated examples for a Feathers service that demonstrate robust logging practices.
Firestore client setup and safe document reference
import { initializeApp } from 'firebase-admin/app';
import { getFirestore, doc, getDoc, updateDoc } from 'firebase-admin/firestore';
initializeApp();
const db = getFirestore();
function docRef(collectionName, id) {
return doc(db, collectionName, id);
}
Logging a Firestore get in a Feathers before hook
// In your Feathers before hook
async function logGetHook(context) {
const { id } = context.params.query || {};
const traceId = context.traceId || 'unknown-trace';
const time = new Date().toISOString();
if (id) {
const documentRef = docRef('tickets', id);
const snapshot = await getDoc(documentRef);
const exists = snapshot.exists();
console.info(JSON.stringify({
timestamp: time,
level: 'info',
service: 'tickets',
operation: 'get',
traceId,
documentId: id,
exists,
fieldsLogged: exists ? Object.keys(snapshot.data()) : []
}));
}
return context;
}
Logging Firestore updates with change summary in a Feathers after hook
async function logUpdateHook(context) {
const { id } = context.result; // document ID from after hook result
const previous = context.data.previous || {};
const current = context.result.data || {};
const traceId = context.traceId || 'unknown-trace';
const time = new Date().toISOString();
const changes = {};
for (const key of new Set([...Object.keys(previous), ...Object.keys(current)])) {
if (previous[key] !== current[key]) {
changes[key] = { from: previous[key], to: current[key] };
}
}
console.info(JSON.stringify({
timestamp: time,
level: 'info',
service: 'tickets',
operation: 'update',
traceId,
documentId: id,
changes
}));
return context;
}
Integrating hooks into a Feathers service with Firestore
const { Service, ServiceOptions } = require('feathersjs');
class TicketsService {
constructor(options: ServiceOptions = {}) {
this.options = options;
}
async find(params) {
const collectionRef = docRef('tickets');
const snapshot = await getDocs(collectionRef);
const data = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
return data;
}
async get(id, params) {
const documentRef = docRef('tickets', id);
const snapshot = await getDoc(documentRef);
if (!snapshot.exists()) {
throw new NotFound('Ticket not found');
}
return { id: snapshot.id, ...snapshot.data() };
}
async create(data, params) {
const collectionRef = docRef('tickets');
const docRef = await addDoc(collectionRef, data);
return { id: docRef.id, ...data };
}
async update(id, data, params) {
const documentRef = docRef('tickets', id);
await updateDoc(documentRef, data);
const snapshot = await getDoc(documentRef);
return { id: snapshot.id, ...snapshot.data() };
}
async patch(id, data, params) {
const documentRef = docRef('tickets', id);
const snapshot = await getDoc(documentRef);
if (!snapshot.exists()) {
throw new NotFound('Ticket not found');
}
const merged = { ...snapshot.data(), ...data };
await updateDoc(documentRef, merged);
return { id: snapshot.id, ...merged };
}
async remove(id, params) {
const documentRef = docRef('tickets', id);
await deleteDoc(documentRef);
return { id };
}
}
// Register hooks
const service = new TicketsService();
service.hooks({
before: {
all: [],
find: [logGetHook],
get: [logGetHook],
create: [],
update: [],
patch: [],
remove: []
},
after: {
all: [],
get: [logUpdateHook],
update: [logUpdateHook],
patch: [logUpdateHook]
},
error: {
all: [],
all: async context => {
console.error(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'error',
service: 'tickets',
error: context.error && context.error.message,
stack: context.error && context.error.stack
}));
}
}
});
Operational recommendations
- Include trace identifiers (traceId) in logs to correlate requests across services and with Firestore operations.
- Structure log entries as JSON and ship them to a centralized system for analysis and alerting.
- Scrub sensitive fields before logging; use hashes or redaction for identifiers that must be retained for debugging.
- Log both successes and failures, including permission-denied cases that may indicate misconfigured Firestore rules.