Dns Rebinding in Express with Firestore
Dns Rebinding in Express with Firestore — how this specific combination creates or exposes the vulnerability
DNS rebinding is an application-layer attack where an attacker causes a victim’s browser to bypass same-origin policy by switching the IP address of a seemingly trusted hostname after initial resolution. In an Express service that interacts with Google Cloud Firestore, this can expose sensitive data or allow unauthorized operations when the client-side code or server-side endpoints trust hostnames or IPs without additional validation.
Consider an Express app that serves a web UI or provides a server-side endpoint that forwards Firestore requests based on parameters provided by the client. If the app resolves a hostname (e.g., internal.firestore.example.com) once and then uses the resulting IP to make privileged Firestore calls, an attacker can serve a page that causes the browser to re-resolve that hostname to a different IP — such as an internal service or a malicious server the attacker controls — after the initial DNS lookup. Because the browser’s same-origin policy is based on origin (scheme + hostname + port), the victim’s browser may still attach cookies or authentication tokens to the rebinded origin, allowing the attacker to make requests that appear to come from a trusted source.
In the context of Firestore, this becomes critical when an Express endpoint uses service account credentials or ID tokens from the client to construct Firestore clients. For example, an endpoint might accept a project ID or document path from the client and then create a Firestore client to read or write data. If the endpoint does not validate and strictly scope the requested document path, an attacker can use DNS rebinding to manipulate the client-supplied hostname or IP used in the request flow, potentially redirecting Firestore operations to a different project or instance that the original token has access to.
An example attack chain:
- An authenticated user loads a page from the Express app that includes a hostname controlled by the attacker (e.g., via a query parameter that sets a Firestore host or project URL).
- The attacker’s DNS returns an initial IP that belongs to a benign service, then switch to an internal IP or a server they control on subsequent requests.
- The victim’s browser sends cookies for the Express origin with the rebinded request, allowing the attacker to perform Firestore actions under the user’s permissions.
Because Firestore security rules and IAM bindings are evaluated per request, the server must ensure that hostnames, origins, and resource paths are validated independently of DNS resolution. Relying on the client to provide hostnames or partial URLs without strict allowlisting can enable DNS rebinding to bypass intended access controls.
Firestore-Specific Remediation in Express — concrete code fixes
To mitigate DNS rebinding risks in an Express application that uses Firestore, validate and scope all inputs that influence Firestore project, instance, or document selection. Do not trust client-supplied hostnames or origins. Use strict allowlists, enforce same-origin checks on the server, and avoid dynamically constructing Firestore clients or document paths from unvalidated inputs.
Below are concrete, Firestore-specific remediation examples for Express.
1. Validate and scope document paths on the server
Instead of passing a document path from the client and appending it to a Firestore reference, define a fixed set of permissible document patterns and validate on the server. Use UID-based scoping so users can only access their own data.
const { initializeApp, cert } = require('firebase-admin');
const express = require('express');
const app = express();
initializeApp({
credential: cert(require('./service-account.json')),
});
const db = require('firebase-admin').firestore();
app.get('/user-data/:userId', async (req, res) => {
const { userId } = req.params;
// Validate userId format to prevent path traversal or impersonation
if (!/^[a-zA-Z0-9_-]{1,36}$/.test(userId)) {
return res.status(400).send('Invalid user ID');
}
const userDoc = await db.collection('users').doc(userId).get();
if (!userDoc.exists) {
return res.status(404).send('Not found');
}
res.json(userDoc.data());
});
app.listen(3000);
2. Enforce origin and referer checks on sensitive endpoints
Add server-side checks for Origin and Referer headers to reduce the risk that a request originated from a rebinded context. Combine this with CORS policies that do not use wildcards for credentials.
app.use('/firestore-action', (req, res, next) => {
const allowedOrigin = 'https://app.example.com';
const origin = req.headers.origin;
const referer = req.headers.referer || '';
if (origin !== allowedOrigin || !referer.startsWith(allowedOrigin)) {
return res.status(403).send('Forbidden');
}
next();
});
app.post('/firestore-action', async (req, res) => {
// Process Firestore action safely with validated inputs
res.send('OK');
});
3. Avoid dynamic hostnames when initializing Firestore clients
Do not construct Firestore app instances or URLs from client-supplied values. If you must work with multiple projects, predefine allowed project identifiers and map them to pre-initialized apps or service account keys on the server.
const projects = {
production: 'my-prod-project',
staging: 'my-staging-project',
};
app.post('/query', async (req, res) => {
const { projectKey, documentId } = req.body;
if (!projects[projectKey]) {
return res.status(400).send('Invalid project');
}
// Use a pre-authorized client for the selected project; do not build a client from a hostname
const projectDb = db; // In practice, use a scoped client or pre-initialized app per project
const doc = await projectDb.collection('items').doc(documentId).get();
res.json(doc.exists ? doc.data() : {});
});
4. Use Firestore security rules to enforce document-level permissions
Even when using server-side clients, ensure Firestore rules restrict reads and writes to allowed document paths and authenticated requests. Treat server-side code as trusted but still validate inputs to avoid accidental privilege escalation.
// Firestore rules example (not JavaScript, shown for context)
// rules_version = '2';
// service cloud.firestore {
// match /databases/{database}/documents {
// match /users/{userId} {
// allow read, write: if request.auth != null && request.auth.uid == userId;
// }
// }
// }
5. Sanitize and normalize inputs before using them in Firestore queries
Normalize user input to prevent encoding-based bypasses. Reject or encode inputs that contain dot segments, unexpected colons, or patterns that could lead to unintended document paths.
function normalizeDocumentPath(input) {
// Remove dot-segments and enforce a simple pattern
return input.replace(/(\.\.?\/|\/)/g, '_');
}
app.post('/doc', async (req, res) => {
const raw = req.body.path;
const safe = normalizeDocumentPath(raw);
const doc = await db.doc(`collection/${safe}`).get();
res.json(doc.data() || {});
});