Skip to content

emulator: reject cross-origin calls to Hub state-changing endpoints#10595

Open
adilburaksen wants to merge 1 commit into
firebase:mainfrom
adilburaksen:fix/emulator-hub-origin-guard
Open

emulator: reject cross-origin calls to Hub state-changing endpoints#10595
adilburaksen wants to merge 1 commit into
firebase:mainfrom
adilburaksen:fix/emulator-hub-origin-guard

Conversation

@adilburaksen
Copy link
Copy Markdown

Description

The Emulator Hub's state-changing endpoints can be driven cross-site from a page open in the developer's browser.

POST /_admin/export and POST /dataconnect/clearData already try to block browser callers with an if (req.headers.origin) 403, but the guard is missing a return — the 403 is sent, yet execution falls through and the side effect (full emulator data export / Data Connect wipe) runs anyway. The trailing res.status(200) then throws ERR_HTTP_HEADERS_SENT, which crashes the emulators:start process.

PUT /functions/disableBackgroundTriggers and /functions/enableBackgroundTriggers have no origin guard at all.

Fix

  • Add the missing return after the 403 in /_admin/export and /dataconnect/clearData so the guard actually stops execution.
  • Apply the same origin guard to the two BackgroundTriggers endpoints.

The legitimate caller (HubClient, a server-side apiv2 node client) does not send an Origin header, so the CLI and the Emulator UI are unaffected — only browser-originated cross-site requests are rejected.

Note (defense-in-depth, not in this PR)

The broader exposure — cors({ origin: true }) on the data emulators (cross-origin reads) and the absence of Host-header validation (DNS-rebinding) — is a larger design question that interacts with the Emulator UI and the tunnel support added in #4227. Happy to follow up separately if that's useful; this PR fixes the clear control-bypass bug.

The export and clearData handlers send a 403 when an Origin header is
present but are missing a `return`, so the side effect (full data export /
Data Connect wipe) runs anyway; the trailing res.status(200) then throws
ERR_HTTP_HEADERS_SENT and crashes the emulators:start process. The
BackgroundTriggers toggle endpoints have no origin guard at all.

Add the missing returns and apply the same origin guard to the two
BackgroundTriggers endpoints. HubClient (the server-side apiv2 node caller)
does not send an Origin header, so the CLI and Emulator UI are unaffected;
only browser-originated cross-site requests are rejected.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds missing return statements after 403 responses in the export and clear SQL Connect endpoints, and introduces checks to block external callers on the Cloud Functions enable/disable endpoints. The reviewer noted that unconditionally blocking requests with an Origin header will break the Emulator UI's cross-origin requests, and provided suggestions to validate the origin against the registered Emulator UI instead.

Comment thread src/emulator/hub.ts
Comment on lines +138 to +143
if (req.headers.origin) {
res.status(403).json({
message: `Cloud Functions triggers cannot be toggled by external callers.`,
});
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Blocking all requests with an Origin header will break the Emulator UI's ability to toggle background triggers. Since the Emulator UI runs in the browser (typically on port 4000) and the Hub runs on a different port (typically 4400), any request from the UI to the Hub is cross-origin and will include an Origin header.

To prevent CSRF/cross-origin attacks from malicious third-party sites while keeping the Emulator UI fully functional, we should validate the Origin header against the registered Emulator UI's origin.

Suggested change
if (req.headers.origin) {
res.status(403).json({
message: `Cloud Functions triggers cannot be toggled by external callers.`,
});
return;
}
if (req.headers.origin) {
const uiInfo = EmulatorRegistry.getInfo(Emulators.UI);
let isAuthorized = false;
try {
const parsed = new URL(req.headers.origin);
isAuthorized = !!uiInfo &&
["localhost", "127.0.0.1", "::1", uiInfo.host].includes(parsed.hostname) &&
parsed.port === String(uiInfo.port);
} catch {}
if (!isAuthorized) {
res.status(403).json({
message: "Cloud Functions triggers cannot be toggled by external callers.",
});
return;
}
}

Comment thread src/emulator/hub.ts
Comment on lines +161 to +166
if (req.headers.origin) {
res.status(403).json({
message: `Cloud Functions triggers cannot be toggled by external callers.`,
});
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Blocking all requests with an Origin header will break the Emulator UI's ability to toggle background triggers. Since the Emulator UI runs in the browser (typically on port 4000) and the Hub runs on a different port (typically 4400), any request from the UI to the Hub is cross-origin and will include an Origin header.

To prevent CSRF/cross-origin attacks from malicious third-party sites while keeping the Emulator UI fully functional, we should validate the Origin header against the registered Emulator UI's origin.

Suggested change
if (req.headers.origin) {
res.status(403).json({
message: `Cloud Functions triggers cannot be toggled by external callers.`,
});
return;
}
if (req.headers.origin) {
const uiInfo = EmulatorRegistry.getInfo(Emulators.UI);
let isAuthorized = false;
try {
const parsed = new URL(req.headers.origin);
isAuthorized = !!uiInfo &&
["localhost", "127.0.0.1", "::1", uiInfo.host].includes(parsed.hostname) &&
parsed.port === String(uiInfo.port);
} catch {}
if (!isAuthorized) {
res.status(403).json({
message: "Cloud Functions triggers cannot be toggled by external callers.",
});
return;
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants