Skip to content

[fix][core] keep server side rendering on the dashboard origin - #7927

Open
ar2rsawseen wants to merge 1 commit into
release.24.05from
backport/render-view-same-origin-2405
Open

[fix][core] keep server side rendering on the dashboard origin#7927
ar2rsawseen wants to merge 1 commit into
release.24.05from
backport/render-view-same-origin-2405

Conversation

@ar2rsawseen

Copy link
Copy Markdown
Member

Backport of #7926 to release.24.05. Same change, the file is identical in the relevant region.

What

api/utils/render.js builds the url it opens by concatenating the configured dashboard host with the requested view:

var host = (process.env.COUNTLY_CONFIG_PROTOCOL || "http") + "://" + (process.env.COUNTLY_CONFIG_HOSTNAME || "localhost") + countlyConfig.path;
...
await page.goto(host + view);

/o/render takes that view from the request (params.qstring.view). With the default countlyConfig.path of "" the prefix is exactly http://localhost, so a view that does not begin with / changes the host rather than the path:

view url actually opened
/#/dashboard http://localhost/#/dashboard, as intended
@169.254.169.254/latest/... host 169.254.169.254
:8500/v1/kv/?recurse host localhost:8500
.internal.example/x host localhost.internal.example

The headless browser then loads that from the server's own network position.

Change

  1. Parse the concatenated url and require its origin to match the dashboard's before navigating. What gets opened is the concatenation itself, unchanged, so a configured countlyConfig.path keeps behaving exactly as it did. Parsing settles the whole family of shapes at once, including the ones the url parser normalises away (tab, newline, carriage return), and it agrees with what Chromium does with the same string since both use the WHATWG parser.
  2. Restrict the renderer to the dashboard origin with request interception, the same control api/utils/pdf.js uses, so the page's subresources are bounded too and not just the navigation.

A private range denylist would be the wrong control here: the intended render target is loopback, which api/utils/ssrf-protection.js blocks by design.

Scope

Everything that drives a headless browser, with a verdict for each:

site verdict
api/utils/render.js, page.goto(host + view) fixed here
api/utils/render.js, login token navigation token is server generated and the host comes from config, and it is covered by the interception anyway
api/utils/pdf.js already limited to Countly's own origins
api/utils/requestProcessor.js, the /o/render caller passes params.qstring.view through, now checked at the sink
plugins/dashboards/api/api.js caller hardcodes "/dashboard?ssr=true#/custom/" + id, unaffected and covered by the tests below
countly-enterprise-plugins no render.js and no renderView caller

Neither caller sets options.host, so the base always stays the configured dashboard.

Verification

  • test/unit-tests/api.utils.render.js, 38 cases: every host rewriting shape is refused for each host form, and every view the product itself renders is allowed and returned byte identical. Swapping the check back for plain concatenation fails 13 of them, so the tests do pin the behaviour.
  • Real Chromium measurement of the interception, since the flag removal in 405b982 showed this file is easy to reason about wrongly: own origin image and stylesheet still load, the screenshot is still produced, the off origin request never reaches the other server, and no unhandled rejections are raised.
  • countly-platform's existing live renderView and renderPDF integration suite (test/2.api/99.api.utils.render.js) passes against the patched file, 7 of 7, including both real screenshot cases.
  • Grepped the dashboard shell for external subresources: there are none, the only CDN references in the tree are the populator demo templates, so nothing in a normal render is refused by the origin restriction.

api/utils/render.js builds the url it opens by concatenating the configured dashboard
host with the requested view, and /o/render lets the caller choose that view. With the
default countlyConfig.path of "" the prefix is exactly "http://localhost", so a view that
does not begin with "/" changes the host rather than the path, and the headless browser
opens that instead of the dashboard.

Parse the concatenated url and require its origin to match the dashboard's before
navigating. What gets navigated to is the concatenation itself, unchanged, so a configured
countlyConfig.path keeps behaving exactly as it did. Parsing settles the whole family of
shapes at once, including the ones the url parser normalises away, and it agrees with what
Chromium does with the same string since both use the WHATWG parser.

Also restrict the renderer to the dashboard origin with request interception, the same
control api/utils/pdf.js already uses. The dashboard serves all of its own assets, so
nothing in a normal render is refused: measured against real Chromium, own origin images
and stylesheets still load and the screenshot is still produced.

A private range denylist would be the wrong control here. The intended render target is
loopback, which api/utils/ssrf-protection.js blocks by design.
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.

1 participant