emulator: reject cross-origin calls to Hub state-changing endpoints#10595
emulator: reject cross-origin calls to Hub state-changing endpoints#10595adilburaksen wants to merge 1 commit into
Conversation
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.
There was a problem hiding this comment.
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.
| if (req.headers.origin) { | ||
| res.status(403).json({ | ||
| message: `Cloud Functions triggers cannot be toggled by external callers.`, | ||
| }); | ||
| return; | ||
| } |
There was a problem hiding this comment.
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.
| 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; | |
| } | |
| } |
| if (req.headers.origin) { | ||
| res.status(403).json({ | ||
| message: `Cloud Functions triggers cannot be toggled by external callers.`, | ||
| }); | ||
| return; | ||
| } |
There was a problem hiding this comment.
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.
| 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; | |
| } | |
| } |
Description
The Emulator Hub's state-changing endpoints can be driven cross-site from a page open in the developer's browser.
POST /_admin/exportandPOST /dataconnect/clearDataalready try to block browser callers with anif (req.headers.origin)403, but the guard is missing areturn— the 403 is sent, yet execution falls through and the side effect (full emulator data export / Data Connect wipe) runs anyway. The trailingres.status(200)then throwsERR_HTTP_HEADERS_SENT, which crashes theemulators:startprocess.PUT /functions/disableBackgroundTriggersand/functions/enableBackgroundTriggershave no origin guard at all.Fix
returnafter the 403 in/_admin/exportand/dataconnect/clearDataso the guard actually stops execution.BackgroundTriggersendpoints.The legitimate caller (
HubClient, a server-sideapiv2node client) does not send anOriginheader, 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.