emulator: validate Host header to mitigate DNS-rebinding#10596
emulator: validate Host header to mitigate DNS-rebinding#10596adilburaksen wants to merge 1 commit into
Conversation
Wiz Scan Summary
To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension. |
There was a problem hiding this comment.
Code Review
This pull request introduces host validation middleware across various Firebase emulators (Express-based, Auth, Eventarc, Functions, Storage, and Tasks) to mitigate DNS-rebinding attacks by rejecting requests with untrusted Host headers when bound to loopback interfaces. The feedback suggests improving the robustness of this middleware by retrieving all bound addresses for the storage emulator to prevent breaking remote access/tunneling configurations, and strictly handling potential null or undefined host/address values in isLoopbackAddress and hostValidationMiddleware to align with the repository's strict null safety guidelines.
|
|
||
| // Reject requests with an untrusted Host header to mitigate DNS-rebinding. | ||
| // Only enforced when bound to loopback (see hostValidationMiddleware). | ||
| app.use(hostValidationMiddleware([emulator.getInfo().host])); |
There was a problem hiding this comment.
Passing only [emulator.getInfo().host] to the host validation middleware can break remote access/tunneling configurations.
If the storage emulator is configured to bind to both a loopback address (e.g., 127.0.0.1) and a wildcard/public address (e.g., 0.0.0.0), emulator.getInfo().host may return the loopback address. This causes the middleware to incorrectly assume the emulator is bound only to loopback, enabling host validation enforcement and blocking legitimate remote requests.
To fix this, retrieve all bound addresses from emulator.getInfo().listen if available.
| app.use(hostValidationMiddleware([emulator.getInfo().host])); | |
| const info = emulator.getInfo(); | |
| const listenAddresses = info.listen ? info.listen.map((s) => s.address) : [info.host]; | |
| app.use(hostValidationMiddleware(listenAddresses)); |
| export function isLoopbackAddress(addr: string): boolean { | ||
| const normalized = addr.trim().toLowerCase(); |
There was a problem hiding this comment.
To adhere to strict null safety and defensive programming practices, update isLoopbackAddress to explicitly handle undefined or null values. This prevents potential runtime crashes if an invalid or unconfigured host is passed.
export function isLoopbackAddress(addr: string | undefined | null): boolean {
if (!addr) {
return false;
}
const normalized = addr.trim().toLowerCase();References
- Use strict null checks and handle undefined/null explicitly. (link)
| export function hostValidationMiddleware(bindAddresses: string[]): express.RequestHandler { | ||
| // Cannot determine the bind address(es) — don't break anything. | ||
| if (bindAddresses.length === 0) { | ||
| return (req, res, next) => next(); | ||
| } | ||
|
|
||
| // Operator opted into remote access by binding to a non-loopback address. | ||
| if (!bindAddresses.every((addr) => isLoopbackAddress(addr))) { | ||
| return (req, res, next) => next(); | ||
| } | ||
|
|
||
| const allowedHosts = new Set(["localhost", "::1", "[::1]"]); | ||
| for (const addr of bindAddresses) { | ||
| allowedHosts.add(addr.trim().toLowerCase()); | ||
| } |
There was a problem hiding this comment.
Update hostValidationMiddleware to safely handle potentially undefined or null bind addresses. Filtering out falsy values ensures that the middleware does not crash when processing configuration lists that may contain empty or unresolved host entries.
| export function hostValidationMiddleware(bindAddresses: string[]): express.RequestHandler { | |
| // Cannot determine the bind address(es) — don't break anything. | |
| if (bindAddresses.length === 0) { | |
| return (req, res, next) => next(); | |
| } | |
| // Operator opted into remote access by binding to a non-loopback address. | |
| if (!bindAddresses.every((addr) => isLoopbackAddress(addr))) { | |
| return (req, res, next) => next(); | |
| } | |
| const allowedHosts = new Set(["localhost", "::1", "[::1]"]); | |
| for (const addr of bindAddresses) { | |
| allowedHosts.add(addr.trim().toLowerCase()); | |
| } | |
| export function hostValidationMiddleware(bindAddresses: (string | undefined | null)[]): express.RequestHandler { | |
| const cleanAddresses = (bindAddresses || []).filter((addr): addr is string => !!addr); | |
| // Cannot determine the bind address(es) — don't break anything. | |
| if (cleanAddresses.length === 0) { | |
| return (req, res, next) => next(); | |
| } | |
| // Operator opted into remote access by binding to a non-loopback address. | |
| if (!cleanAddresses.every((addr) => isLoopbackAddress(addr))) { | |
| return (req, res, next) => next(); | |
| } | |
| const allowedHosts = new Set(["localhost", "::1", "[::1]"]); | |
| for (const addr of cleanAddresses) { | |
| allowedHosts.add(addr.trim().toLowerCase()); | |
| } |
References
- Use strict null checks and handle undefined/null explicitly. (link)
Description
The local emulator HTTP servers all use
cors({ origin: true })and perform noHost-header validation. A web page open in a developer's browser can therefore reach the (unauthenticated) emulator APIs via DNS-rebinding — rebind an attacker-controlled domain to127.0.0.1, after which requests are same-origin so CORS no longer applies — and drive the emulator endpoints (read/modify/delete local emulator data, toggle Cloud Functions triggers, etc.).Fix
Add a small
hostValidationMiddleware(src/emulator/hostValidation.ts) that rejects requests whoseHostheader is not a loopback/configured host, wired as the first middleware on every emulator's express app — Hub/UI viaExpressBasedEmulator, plus Auth, Storage, Eventarc, Tasks, and Functions.Enforcement happens only when the emulator is bound exclusively to loopback addresses (the rebinding-vulnerable default). If the operator bound to a non-loopback address (
0.0.0.0,::, or a specific host) they have opted into remote access — e.g. the tunnel support from #4227 — so the middleware becomes a no-op and those setups keep working.The legitimate callers (the
HubClientnode client, the CLI, and the Emulator UI) send loopbackHostheaders and requests without aHostheader are allowed, so they are unaffected.Testing
tsc --noEmit) passes; files are prettier-clean.Host: localhost/127.0.0.1/::1/[::1]/ the configured host (and requests with noHost), and returns403for an untrusted host (e.g. a rebound attacker domain); under a non-loopback bind it is a pass-through.