Skip to content

Validate region URLs before failing over to them - #1270

Open
xianshijing-lk wants to merge 1 commit into
mainfrom
security/validate-region-urls
Open

xianshijing-lk wants to merge 1 commit into
mainfrom
security/validate-region-urls

Conversation

@xianshijing-lk

Copy link
Copy Markdown
Contributor

Flutter port of livekit/rust-sdks#1601, livekit/client-sdk-swift#1182 and livekit/client-sdk-android#1052.

What

isCloud() is checked against the server URL the application supplied, to decide whether region discovery applies at all. The URLs that came back were then used as they arrived — fetchRegionSettings builds a RegionInfo straight from region['url'], and getNextBestRegionUrl() hands that to the connect path.

Those URLs are not just connected to, they are authenticated to: fetchRegionSettings sends the participant token as a bearer header. An entry should stand on its own merits rather than inherit trust from the configured server URL, and it should not be able to move that exchange onto cleartext.

Two changes

1. Validate returned region URLs. New isTrustedRegionUrl: must parse, have a host, name a .livekit.cloud / .livekit.run host, and use wss or https. Applied to both sources of a region list — the /settings/regions response and setServerReportedRegions, which takes the regions the server reports over signalling. Both were unvalidated. Rejections are logged, not silent.

2. isCloudUrl matched with contains, not a suffix. This one is Flutter-specific — Rust, Swift and Android all match on the suffix:

// before
return uri.host.contains('.livekit.cloud') || uri.host.contains('.livekit.run');

So any host with .livekit.cloud anywhere inside it counted as LiveKit Cloud. project.livekit.cloud.example.com is someone else's host, and treating it as Cloud sends it the region request with the token attached. Now matched on the suffix, with the host lower-cased first.

That second change also tightens isCloudUrl's four other callers: the region gate in Room, and the TURN, websocket and cloud-region connection checks. Worth a reviewer's eye — it is strictly more restrictive, and matches the other three SDKs.

Tests

Twelve tests covering both predicates across cloud hosts, cleartext schemes, foreign hosts, suffix near-misses and malformed input, plus one covering the server-reported ingestion path.

The suffix test and the ingestion test were both verified to fail without this change.

Full suite: 501 tests pass. flutter analyze, dart format and import_sorter all clean.

🤖 Generated with Claude Code

`isCloud()` is checked against the server URL the application supplied,
to decide whether region discovery applies at all. The URLs that came
back were then used as they arrived: `fetchRegionSettings` builds a
`RegionInfo` straight from `region['url']`, and `getNextBestRegionUrl()`
hands that to the connect path.

Those URLs are not just connected to, they are authenticated to --
`fetchRegionSettings` sends the participant token as a bearer header. An
entry should stand on its own merits rather than inherit trust from the
configured server URL, and it should not be able to move that exchange
onto cleartext.

Add `isTrustedRegionUrl`: the URL must parse, have a host, name a
`.livekit.cloud` or `.livekit.run` host, and use `wss` or `https`. Apply
it to both sources of a region list -- the `/settings/regions` response
and `setServerReportedRegions`, which takes the regions the server
reports over signalling. Both were unvalidated. Rejected entries are
logged rather than dropped silently.

Separately, `isCloudUrl` matched the Cloud domains with `contains`, so
any host with `.livekit.cloud` anywhere inside it counted as LiveKit
Cloud -- `project.livekit.cloud.example.com` is someone else's host, and
treating it as Cloud sends it the region request with the token
attached. It now matches on the suffix, like the Rust, Swift and Android
SDKs, and lower-cases the host first.

The suffix change also tightens `isCloudUrl`'s four other callers: the
region gate in `Room`, and the TURN, websocket and cloud-region
connection checks.

Twelve tests: the two predicates across cloud hosts, cleartext schemes,
foreign hosts, suffix near-misses and malformed input, plus one covering
the server-reported ingestion path. The suffix test and the ingestion
test both fail without this change.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

if (uri == null || uri.host.isEmpty) return false;
final scheme = uri.scheme.toLowerCase();
if (scheme != 'wss' && scheme != 'https') return false;
return isCloudUrl(uri);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟥 Region URLs can redirect bearer tokens

When a region URL points to a Cloud host that redirects elsewhere, isTrustedRegionUrl accepts it before connecting. Redirect-following HTTP requests can forward the participant token to the redirect target.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

This branch has not been deployed

No deployments
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