HIGH insufficient loggingsailsfirestore

Insufficient Logging in Sails with Firestore

Insufficient Logging in Sails with Firestore — how this specific combination creates or exposes the vulnerability

Insufficient logging in a Sails application that uses Google Cloud Firestore as a persistence layer reduces visibility into authentication, authorization, and data access events. Without structured, tamper-resistant logs, incident responders lack the details needed to reconstruct an attack chain, correlate suspicious activity, or validate that security controls are behaving as intended.

In this stack, the application typically interacts with Firestore via the Firebase Admin SDK or the Google Cloud client libraries. If Sails controllers, services, or policies do not explicitly log identity context, request parameters, Firestore operation outcomes, and error details, critical signals are lost. For example, a missing log entry for a failed attempt to read a document due to insufficient user permissions can hide an Insecure Direct Object Reference (IDOR) or BOLA attempt. Similarly, lack of audit logs for administrative functions (such as updating or deleting documents) means abuse may go undetected.

The interaction with Firestore amplifies certain risks. Firestore offers flexible querying and real-time listeners; without corresponding logs for query origins, requested paths, and returned data scopes, it is harder to detect anomalous data access patterns. Unauthenticated endpoints or overly permissive Firestore rules may allow broad read/write access; if Sails does not log these requests with user or IP context, attackers can probe the API without leaving an actionable trace. Moreover, insufficient logging can obscure issues such as missing input validation on Firestore document IDs, which may lead to unauthorized document access or injection-like behaviors when malformed data is processed.

Compliance and operational visibility are also affected. Frameworks such as OWASP API Top 10 include logging and monitoring as a control; PCI-DSS, SOC2, HIPAA, and GDPR similarly expect audit trails for access to personal or sensitive data. When Sails does not produce reliable logs for Firestore interactions, meeting these requirements becomes difficult. Effective logging must capture who accessed what, when, and with what outcome, while ensuring logs themselves are protected from tampering and retention is managed appropriately.

Firestore-Specific Remediation in Sails — concrete code fixes

Remediation focuses on instrumenting Sails controllers and services to emit structured logs for every Firestore interaction, including request metadata, operation results, and errors. Use a logging library that supports structured output (e.g., winston or pino) and ensure logs are centralized and protected.

Example: Logging reads and writes with the Firebase Admin SDK

const admin = require('firebase-admin');
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console(),
    // configure additional transports to ship logs to your SIEM or log management
  ],
});

// Initialize Firebase Admin if not already done
if (!admin.apps.length) {
  admin.initializeApp({
    credential: admin.credential.applicationDefault(),
  });
}

const db = admin.firestore();

module.exports = {
  readDocument: async function readDocument(collectionName, documentId, requestContext) {
    const logBase = {
      collection: collectionName,
      documentId: documentId,
      requestId: requestContext.requestId || null,
      userId: requestContext.user ? requestContext.user.id : null,
      ip: requestContext.ip || null,
    };

    try {
      const docRef = db.collection(collectionName).doc(documentId);
      const doc = await docRef.get();

      if (!doc.exists) {
        logger.warn('Document not found', { ...logBase, found: false });
        return null;
      }

      logger.info('Document read successfully', { ...logBase, found: true, dataKeys: Object.keys(doc.data()) });
      return doc.data();
    } catch (error) {
      logger.error('Error reading document from Firestore', { ...logBase, error: error.message, code: error.code });
      throw error;
    }
  },

  writeDocument: async function writeDocument(collectionName, documentId, data, requestContext) {
    const logBase = {
      collection: collectionName,
      documentId: documentId,
      requestId: requestContext.requestId || null,
      userId: requestContext.user ? requestContext.user.id : null,
      ip: requestContext.ip || null,
    };

    try {
      const docRef = db.collection(collectionName).doc(documentId);
      await docRef.set(data, { merge: true });
      logger.info('Document written to Firestore', { ...logBase, operation: 'set', merge: true });
    } catch (error) {
      logger.error('Error writing document to Firestore', { ...logBase, error: error.message, code: error.code });
      throw error;
    }
  },

  queryDocuments: async function queryDocuments(collectionName, whereClause, requestContext) {
    const logBase = {
      collection: collectionName,
      requestId: requestContext.requestId || null,
      userId: requestContext.user ? requestContext.user.id : null,
      ip: requestContext.ip || null,
    };

    try {
      let query = db.collection(collectionName);
      if (whereClause && whereClause.field && whereClause.op && whereClause.value !== undefined) {
        query = query.where(whereClause.field, whereClause.op, whereClause.value);
      }
      const snapshot = await query.get();

      logger.info('Query executed on Firestore', {
        ...logBase,
        resultCount: snapshot.size,
        queryConstraints: query._query._queryProto.structuredQuery.where
          ? JSON.stringify(query._query._queryProto.structuredQuery.where)
          : null,
      });

      const results = [];
      snapshot.forEach((doc) => results.push({ id: doc.id, ...doc.data() }));
      return results;
    } catch (error) {
      logger.error('Error querying Firestore', { ...logBase, error: error.message, code: error.code });
      throw error;
    }
  },
};

In the examples above, each log entry includes a consistent set of metadata: the collection and document identifiers, the user context (if available), the source IP, and a request identifier for tracing. This makes it easier to correlate logs with Firestore operations and to detect patterns such as repeated failed reads on non-existent documents or unexpected write volumes for specific users.

Instrumenting Sails policies to log authorization decisions

Sails policies are a natural place to emit authorization-related logs. Below is a policy that logs allow/deny decisions with sufficient context for audits.

module.exports.authzLogger = function (req, res, next) {
  const requestContext = {
    requestId: req.id || null,
    userId: (req.user && req.user.id) || null,
    ip: req.ip || null,
    method: req.method,
    url: req.url,
  };

  // Example: allow if user owns the resource; deny otherwise
  if (req.user && req.param('userId') && req.user.id === req.param('userId')) {
    logger.info('Authorization allowed', { ...requestContext, reason: 'owner_match' });
    return next();
  }

  logger.warn('Authorization denied', { ...requestContext, reason: 'not_owner' });
  return res.forbidden('Access denied');
};

Use this policy in controllers to ensure that authorization decisions tied to Firestore operations are recorded. Combine these logs with Firestore audit logs (available via Cloud Audit Logs) to achieve defense-in-depth visibility.

Correlating Sails logs with Firestore audit logs

Firestore provides Cloud Audit Logs for Admin read/write activity. While Sails logs provide application-layer context (user intent, business logic outcomes), pairing them with Firestore audit entries gives a complete picture. Ensure log aggregation pipelines include correlation identifiers (such as requestId) to stitch application logs and platform audit logs together during investigations.

Frequently Asked Questions

What should I include in structured logs for Firestore operations in Sails?
Include correlation identifiers (requestId), user context (userId, roles), source IP, collection and document identifiers, operation type (read/write/query), outcome (success/failure), and sanitized error details. Avoid logging full request or response bodies that may contain PII.
How can I avoid performance impact when adding Firestore logging to Sails?
Use asynchronous, non-blocking logging libraries, batch log writes when possible, and sample high-volume events appropriately. Ensure log transport is resilient and does not block the event loop or introduce timeouts in Firestore operations.