Injection Flaws in Restify with Firestore
Injection Flaws in Restify with Firestore — how this specific combination creates or exposes the vulnerability
Injection flaws occur when untrusted data is interpreted as part of a command or query. In a Restify service that uses Google Cloud Firestore, the most common pattern is accepting client-supplied values and using them to construct Firestore queries. If these values are not strictly validated, sanitized, or parameterized, an attacker can manipulate query behavior.
Firestore itself does not support traditional SQL-style injection because it is a NoSQL database with a strongly typed query API. However, injection-like issues arise at the application layer when building queries dynamically. For example, using string concatenation or object spread to inject field names, collection paths, or operator values from user input can lead to unintended data access or data leakage.
Consider a Restify endpoint that retrieves a user document by ID:
server.get('/users/:id', async (req, res, next) => {
const userDoc = await db.collection('users').doc(req.params.id).get();
res.send(userDoc.data());
});
At first glance this appears safe because the document ID is used as a literal path component. The risk increases when the query structure itself becomes dynamic. An endpoint that filters on a field supplied by the client can become vulnerable:
server.get('/search', async (req, res, next) => {
const { field, value } = req.query;
const q = db.collection('products').where(field, '==', value);
const snapshot = await q.get();
const results = snapshot.docs.map(d => d.data());
res.send(results);
});
If field is taken directly from req.query, an attacker can supply a field name that exposes sensitive documents or metadata. For instance, supplying field=__proto__ or other special keys can distort object behavior in JavaScript, potentially bypassing intended filters. More critically, if the application builds more complex dynamic queries using unsanitized input, it may inadvertently allow access to collections or documents that should be restricted.
Another injection pattern involves operator injection. If an endpoint allows the client to specify both field and operator, and the operator is not strictly enumerated, an attacker might supply values such as {'$gt': 0} to manipulate query semantics in unexpected ways. Firestore query constraints require that the operator be one of a fixed set (==, <, <=, >, >=, array-contains, etc.). Allowing raw user input to dictate the operator effectively lets the client reshape the query logic.
Path traversal is also a concern when collection or document IDs are derived from user input. For example, constructing a document reference using concatenation or template strings without strict validation can allow traversal across logical boundaries:
const docPath = `users/${userId}/data/${documentId}`;
const doc = await db.doc(docPath).get();
If userId or .. or encoded equivalents, it may reference an unintended document. While Firestore treats the path as a single string, the application logic must ensure that IDs are validated and scoped correctly.
In summary, injection flaws with Restify and Firestore stem from dynamically constructing queries, field names, operators, or document paths using unsanitized client input. The database does not execute arbitrary code, but the application can be coerced into reading or querying data in unintended ways, leading to information exposure or privilege escalation.
Firestore-Specific Remediation in Restify — concrete code fixes
Remediation centers on strict input validation, whitelisting, and avoiding dynamic query construction. Below are concrete, Firestore-specific patterns for Restify that reduce injection risk.
1. Use a strict allowlist for field names
Do not trust client-supplied field names. Validate against a known set of fields before using them in a query:
const ALLOWED_FIELDS = new Set(['name', 'price', 'category', 'createdAt']);
server.get('/search', async (req, res, next) => {
const { field, value } = req.query;
if (!ALLOWED_FIELDS.has(field)) {
return next(new Error('Invalid field')); // or return a 400 response
}
const q = db.collection('products').where(field, '==', value);
const snapshot = await q.get();
const results = snapshot.docs.map(d => d.data());
res.send(results);
});
2. Use a fixed set of validated operators
If your API must support dynamic operators, map user input to known safe values:
const OPERATOR_MAP = {
eq: '==',
gt: '>',
gte: '>=',
lt: '<',
lte: '<=',
in: 'in'
};
server.get('/advanced-search', async (req, res, next) => {
const { field, op, value } = req.query;
const operator = OPERATOR_MAP[op];
if (!operator || !ALLOWED_FIELDS.has(field)) {
return next(new Error('Invalid operator or field'));
}
// Firestore requires using the operator constant from the SDK
// This example assumes a mapping to the actual Firestore operator
const q = db.collection('products').where(field, operator, Number(value));
const snapshot = await q.get();
res.send(snapshot.docs.map(d => d.data()));
});
3. Validate and scope document paths
Ensure IDs are alphanumeric (or match an expected pattern) and avoid string concatenation for paths. Use a helper to construct safe references:
function safeDocPath(userId, documentId) {
if (!/^[a-zA-Z0-9_-]+$/.test(userId) || !/^[a-zA-Z0-9_-]+$/.test(documentId)) {
throw new Error('Invalid ID');
}
return db.doc(`users/${userId}/data/${documentId}`);
}
server.get('/data/:userId/:documentId', async (req, res, next) => {
const docRef = safeDocPath(req.params.userId, req.params.documentId);
const doc = await docRef.get();
if (!doc.exists) {
return next(new Error('Not found'));
}
res.send(doc.data());
});
4. Prefer parameterized queries and avoid raw eval
Never construct Firestore queries by evaluating strings or using Function. Stick to the SDK’s methods with explicit parameters.
5. Use middleware to normalize and sanitize inputs
Add a validation layer before handlers run. For example, using a simple validator:
function validateQuery(req, res, next) {
const { field } = req.query;
if (field && !/^[a-z][a-zA-Z0-9]*$/.test(field)) {
return res.status(400).send({ error: 'Invalid query parameter' });
}
next();
}
server.get('/search', validateQuery, async (req, res, next) => {
const q = db.collection('products').where(req.query.field, '==', req.query.value);
const snapshot = await q.get();
res.send(snapshot.docs.map(d => d.data()));
});
These patterns ensure that Firestore queries remain predictable and that user input cannot alter the intended scope or structure of the query.