Log Injection in Adonisjs with Firestore
Log Injection in Adonisjs with Firestore — how this specific combination creates or exposes the vulnerability
Log injection occurs when untrusted data is written directly into application logs without sanitization, enabling attackers to forge log entries, obscure real events, or trigger log-based attacks such as log forging and log injection. In an AdonisJS application that uses Firestore as a backend datastore, this risk arises when request-driven data—such as user identifiers, query parameters, or dynamic document paths—is written to logs without validation or escaping.
AdonisJS does not inherently sanitize values before they reach your logging layer, and Firestore operations often include dynamic values (e.g., document IDs, collection names, query constraints) that originate from user input. When these values are interpolated into log strings, an attacker can inject newline characters or crafted payloads that alter the structure of log entries. For example, a malicious document ID containing a newline can cause a single log line to appear as multiple entries, which may bypass log monitoring rules or hide related suspicious events.
Consider an endpoint that retrieves a Firestore document by ID and logs the operation. If the document ID is taken directly from request inputs and concatenated into a log message, an attacker-supplied ID like attacker\nINFO: Privilege escalation attempted can fabricate additional log lines. Because the logs may later be ingested by external monitoring tools, the injected content can distort metrics, trigger false alerts, or obscure genuine malicious activity. In Firestore-heavy services, this becomes more critical because document IDs and field values often drive business logic, and their presence in logs is common for debugging.
Additionally, Firestore queries in AdonisJS may include user-controlled filters that are logged for audit purposes. If these filters are logged verbatim—such as the where clause values used in a query—an attacker can inject structured text that misrepresents the actual query intent. This can complicate incident response, as security teams may rely on log integrity to detect unauthorized access patterns or data scraping behavior.
The combination of AdonisJS’s flexible logging practices and Firestore’s document-centric model amplifies the impact of log injection. Because logs often correlate document paths, timestamps, and user actions, injected content can break correlation logic and reduce the effectiveness of log-based detection. Real-world patterns such as CVE-related log forging techniques highlight how seemingly benign log entries can be weaponized when input validation is absent.
Firestore-Specific Remediation in Adonisjs — concrete code fixes
To mitigate log injection in AdonisJS applications using Firestore, you must sanitize all dynamic values before they reach log statements. This includes document IDs, collection names, query parameters, and any user-controlled data used in Firestore operations. Below are concrete, Firestore-aware remediation patterns with syntactically correct code examples.
1. Sanitize document IDs and log structured metadata
Instead of directly interpolating Firestore document IDs into log messages, normalize them and log key-value pairs separately. This preserves auditability while preventing newline or control-character injection.
const { Firestore } = require('@google-cloud/firestore');
const firestore = new Firestore();
const logger = use('Logger');
async function getDocumentSafely(ctx) {
const rawId = ctx.request.param('id');
const safeId = Buffer.from(rawId).toString('base64'); // basic normalization
const docRef = firestore.collection('items').doc(rawId);
const doc = await docRef.get();
// Structured logging avoids string injection
logger.info('firestore_document_fetch', {
collection: 'items',
documentId: safeId,
exists: doc.exists,
timestamp: new Date().toISOString()
});
ctx.response.json(doc.data());
}
2. Parameterize Firestore queries and log only sanitized filters
When logging query constraints, extract and sanitize values individually. Avoid logging raw request payloads that may contain newlines or delimiters.
async function listProducts(ctx) {
const { category, minPrice } = ctx.request.only(['category', 'minPrice']);
const collection = firestore.collection('products');
let query = collection;
if (category) {
// Validate category to prevent injection of control characters
const safeCategory = category.replace(/[\r\n]+/g, '_');
query = query.where('category', '==', safeCategory);
}
if (minPrice) {
const numericPrice = Number(minPrice);
if (!Number.isNaN(numericPrice)) {
query = query.where('price', '>=', numericPrice);
}
}
const snapshot = await query.get();
const results = snapshot.docs.map(d => ({ id: d.id, ...d.data() }));
// Log only sanitized, structured filter values
logger.info('firestore_query_execute', {
collection: 'products',
filters: {
category: category ? safeCategory : undefined,
minPrice: numericPrice || undefined
},
resultCount: results.length
});
ctx.response.json(results);
}
3. Centralize log formatting with a custom transport
Implement a small logging wrapper that escapes newlines and control characters for all Firestore-related entries. This ensures consistency across the codebase and prevents accidental bypasses.
const escapeLogValue = (value) => {
if (typeof value !== 'string') return value;
return value.replace(/[\r\n]+/g, ' ').trim();
};
function logFirestoreAction(action, details) {
const safeDetails = Object.entries(details).reduce((acc, [key, val]) => {
acc[key] = Array.isArray(val) ? val.map(escapeLogValue) : escapeLogValue(val);
return acc;
}, {});
logger.info(`firestore_${action}`, safeDetails);
}
// Usage in route handler
logFirestoreAction('document_read', {
collection: 'tickets',
documentId: rawId,
user: ctx.auth.user.id
});
4. Validate Firestore paths and reject dangerous characters
Firestore document paths can be manipulated if IDs contain path-like segments. Reject or encode characters that could simulate subcollections in log contexts.
function validateFirestoreId(id) {
if (!id || typeof id !== 'string') return false;
// Reject slashes and control characters used in paths
return !/[\/\\\r\n]/.test(id) && id.trim() === id;
}
if (!validateFirestoreId(rawId)) {
ctx.throw(400, 'Invalid document identifier');
}