Server Side Template Injection in Loopback with Firestore
Server Side Template Injection in Loopback with Firestore — how this specific combination creates or exposes the vulnerability
Server Side Template Injection (SSTI) occurs when user-controlled data is interpolated into a template that is processed by a template engine. In Loopback applications that use templating to generate dynamic responses—such as emails, reports, or UI fragments—passing unsanitized input into these templates can lead to arbitrary code execution within the template context. When the application also uses Google Firestore as a backend, the risk profile extends to data exposure and unintended interaction with Firestore queries or document structures.
Consider a Loopback controller that builds a status report by fetching a document from Firestore and then passing values into a template. If an attacker can influence the template selection or the data keys used during rendering, they may inject template directives that execute JavaScript within the server-side runtime. For example, providing a crafted input that resolves to something like {{this.constructor.constructor('return process')()}} in a Node.js templating engine can leak sensitive environment information. Because Firestore documents often contain nested objects and arrays, an attacker can probe for deeper template context leakage by traversing properties that originate from Firestore results.
The interaction between Loopback’s controller logic and Firestore’s document model amplifies the impact. If the application dynamically uses Firestore document fields as template variable names without strict allowlisting, an attacker can supply field-like keys (e.g., __proto__, constructor, or constructor.prototype) that modify object inheritance chains. This can lead to prototype pollution or function execution when the template engine evaluates expressions. Moreover, if the application logs or echoes Firestore query metadata (such as collection names or document IDs) into templates, those values become additional injection surfaces.
Real-world exploitation patterns include using SSTI to reach out to internal metadata services or to chain further attacks such as Server-Side Request Forgery (SSRF) via template filters that perform network calls. In a Loopback app, this might manifest as a filter that formats data before rendering and inadvertently triggers an outbound request to a URL derived from Firestore document fields. Because SSTI can execute code in the Node.js process, it may lead to unauthorized access to Firestore credentials stored in environment variables or to exfiltration of sensitive documents through the compromised template context.
Firestore-Specific Remediation in Loopback — concrete code fixes
Mitigating SSTI in Loopback when integrating with Firestore requires strict separation of data and logic, along with disciplined use of templates. Avoid rendering user input directly in templates; instead, use explicit allowlists for fields that can be used in rendering. When using Loopback’s view components, bind only pre-validated data models or DTOs (Data Transfer Objects) that exclude raw Firestore document keys.
Example 1: Safe data projection before template rendering
Instead of passing a raw Firestore document to the template, extract only the necessary fields:
const snapshot = await db.collection('reports').doc('weekly').get();
const data = snapshot.data();
// Explicit projection to safe shape
const safeData = {
title: data.title,
status: data.status,
generatedAt: data.generatedAt.toDate().toISOString(),
};
res.render('status-report', { report: safeData });
Example 2: Using a templating engine with sandboxed context
If you use a templating engine like Handlebars, configure it to disallow prototype access and limit helpers:
const exphbs = require('express-handlebars').create({
helpers: {
safeConcat: function(a, b) { return a + b; },
},
strict: true,
disablePrototypeAccess: true,
});
app.engine('handlebars', exphbs.engine);
app.set('view engine', 'handlebars');
// Controller
app.get('/report', async (req, res) => {
const doc = await db.collection('emails').doc('welcome').get();
const context = {
user: { name: doc.data().name },
};
res.render('welcome', context);
});
Example 3: Validating Firestore document keys before use
When dynamically referencing Firestore fields in any server-side logic, validate keys against an allowlist:
const allowedKeys = new Set(['name', 'email', 'preferences']);
const userData = req.body;
for (const key of Object.keys(userData)) {
if (!allowedKeys.has(key)) {
throw new Error('Invalid field: ' + key);
}
}
// Safe to use userData with known keys
await db.collection('users').doc(currentId).update(userData);
Example 4: Avoiding dynamic template selection
Never derive template names from user input or Firestore document content. Use a fixed mapping:
const templates = {
welcome: 'emails/welcome.hbs',
alert: 'emails/alert.hbs',
};
const templateName = templates[userRole] || 'welcome';
res.render(templateName, { user: userDoc.data() });
Example 5: Securing Firestore-triggered background tasks
If using Firestore triggers (e.g., via Cloud Functions called from Loopback), ensure the trigger payload is validated and does not directly influence template rendering logic:
exports.sendNotification = functions.firestore
.document('messages/{msgId}')
.onCreate(async (snap, context) => {
const data = snap.data();
if (typeof data.text !== 'string' || data.text.length > 500) {
throw new Error('Invalid message payload');
}
// Use a controlled template path
const html = compileTemplate('notifications/new-message', { text: data.text });
await sendEmail(data.recipient, html);
});