Skip to content

fix: allow cleartext http to loopback only - #9782

Draft
mikhail-dcl wants to merge 1 commit into
devfrom
bugsweep/wearable-mainfile-insecure-url
Draft

fix: allow cleartext http to loopback only#9782
mikhail-dcl wants to merge 1 commit into
devfrom
bugsweep/wearable-mainfile-insecure-url

Conversation

@mikhail-dcl

Copy link
Copy Markdown
Collaborator

upgrading other http to ttps unless the request explicitly opts in (local-scene development)

Problem

InvalidOperationException: Insecure connection not allowed on every wearable main-file / profile / content fetch whenever the request URL scheme is http. Sentry UNITY-EXPLORER-P42 (105 ev/wk) + P4G (76 ev/wk), ongoing since May. Every sampled event is the Creator Hub / sdk-commands local preview flow (http://127.0.0.1:8000): release builds systematically break the officially supported local-preview path for every creator.

Root cause

The client's transport-security policy lives solely in Unity's baked, global insecureHttpOption: NotAllowed, which cannot express the policy the product needs — cleartext is fine to loopback (local preview, sidecars), never to real hosts. Not a wearables bug: the wearable frame is just the highest-volume victim of the missing scheme policy at the web-request choke point.

Fix

  1. ProjectSettings.asset: insecureHttpOption → AlwaysAllowed; enforcement moves into the client layer.
  2. WebRequestUtils: pure static policy — EnforceSecureScheme(url) + IsForbiddenCleartext(url). Non-http schemes untouched (zero-alloc https fast path); http+loopback (localhost, 127.0.0.0/8, [::1] via IPAddress.IsLoopback) allowed; http+any other host rewritten to https (those threw before sending at the pin, so the upgrade can only equal or improve the outcome). Unparsable http URLs fail closed.
  3. RequestEnvelope.InitializedWebRequest: secure-scheme enforcement applies to the wire URL of every request, signed and unsigned alike — default-deny is the property that justifies flipping the player setting to AlwaysAllowed. The single exemption is an explicit per-request opt-in: CommonArguments.AllowInsecureCleartext (new readonly field, trailing optional ctor param, default false), set only by the scene fetch() API (SimpleFetchApiImplementation.FetchAsync) in local-scene-development mode. That module already enforces its own dev-mode-aware gate — https required unless isLocalSceneDevelopment — and in dev mode a scene may fetch cleartext http to any host (LAN dev server, remote test endpoint), so the envelope must not rewrite those. Enforcement runs before signing, so a signed request's signature covers the upgraded URL and the identity auth chain never travels over forbidden cleartext; loopback http always passes through, so signed requests against a local preview realm keep working. Per-request composition (the media-converter's embedded http origin query param) is untouched. One warning log per upgrade for support triage.
  4. WebRequestController.SendAsync: redirect-downgrade guard — IsCleartextDowngrade(sentUrl, finalUrl) fires only when the exchange left on an allowed scheme/host (https, or loopback http) and its final (post-redirect) URL is forbidden cleartext; checked on both success and exception paths, so a downgraded exchange is never consumed and never retried. Because every non-opted-in wire URL is upgraded to https before send, the guard covers all of that traffic; an exchange deliberately sent as cleartext (an opted-in local-scene-development fetch) is not a downgrade and is not blocked. This is weaker than the old player setting's pre-send block — Unity's internal redirect handling has already sent the downgraded hop by the time the guard sees it; a true per-hop block would need redirectLimit = 0 + manual redirect handling for signed requests (follow-up). The guard throws a distinct "Insecure redirect blocked" message so these events don't land in the P42/P4G buckets.
  5. WebRequestUtils.IsLocalhost: reimplemented on top of the policy's single loopback definition (Uri-parsed; http/https + localhost/127.0.0.0/8/[::1]) — the old prefix check missed 127.0.0.0/8 beyond 127.0.0.1 (rerouting loopback textures through the public media converter, which cannot reach the creator's machine) and false-positived on localhost.evil.com.
  6. Bypass sites the settings flip would otherwise leave un-gated now apply the same policy: UrlResolverService (scene-controlled media URLs — the resolved URL itself is upgraded, so reachability probes and playback agree on one wire URL, and the raw GET probe enforces the scheme at the emit site) and AbgenSidecar (the realm root behind its /about + catalyst fetches; loopback LSD preview roots pass through unchanged).

Known behavior change: http realms/worlds (#7827 class) now fail as a retried TLS connect error instead of a synchronous "Insecure connection not allowed" — same terminal outcome, different message, slower on probe-style paths.

Test

New EditMode InsecureSchemePolicyShould (40 cases): default-deny locks (UpgradeNonLoopbackHttpToHttpsWhenUnsigned ×2 and UpgradeNonConvertedTextureUrlAtTheWire — unsigned envelope/texture wire URLs are upgraded to https), the dev opt-in lock (PassNonLoopbackHttpThroughUnchangedWhenCleartextAllowed ×2 — with CommonArguments.AllowInsecureCleartext the URL comes out byte-identical), signed-request upgrade (enforce-before-sign) + signed-loopback pass-through, the KeepPlayerSettingAlwaysAllowed lock pairing the settings flip with the client policy, loopback pass-through combinatorial over the opt-in flag (never upgraded either way), https/file pass-through guards, the converter-embedded-origin preservation guard, IsForbiddenCleartext + IsCleartextDowngrade classification cases (downgrade, deliberate-cleartext, redirect-to-loopback), the loopback-texture-stays-direct guard (ktx enabled), and IsLocalhost classification cases (127.0.0.0/8, localhost.evil.com). Plus 3 UrlResolverServiceShould cases: direct non-loopback http upgraded for probe + playback, loopback http kept cleartext.

Validation

Windows Unity 6000.4 EditMode lane at the pin: RED FAIL 3 as intended (non-loopback http stayed http; setting was NotAllowed) + 7 guards passed / GREEN PASS 16/16 for the original round. Rescope round (security review): GREEN (full fix) passes all 40; RED (same tests with the envelope guard reverted to signed-only) fails exactly the 3 default-deny locks — the coverage the review flagged as dropped.

Fixes #3661
Related: #8761, #8762 (same-day auto-closed dupes of P42/P4G), #8080 (MediaStream direct probe — its bypass site now applies the same policy, see Fix 6), #7827 (realm-connect variant; failure message changes as described above)

Includes inspection-warning cleanup in all touched files.

Pull Request Description

What does this PR change?

Test Instructions

Steps (standard run):

metaforge explorer run XXXX  # ← replace with this PR number

Expected result:

Steps (fresh account):

metaforge account create --clear
metaforge explorer run XXXX  # ← replace with this PR number

Expected result:

Automation (if applicable):

metaforge explorer test XXXX

Prerequisites

  • List any required setup steps
  • Include environment/configuration requirements

Test Steps

  1. First step
  2. Second step
  3. Expected result after step 2
  4. ...

Additional Testing Notes

  • Note any edge cases to verify
  • Mention specific areas that need careful testing
  • List known limitations or potential issues

Quality Checklist

  • Changes have been tested locally
  • Documentation has been updated (if required)
  • Performance impact has been considered
  • For SDK features: Test scene is included

Code Review Reference

Please review our Branch & PR Standards before submitting. It explains the automated review flow, QA/DEV approval requirements, and what each label does — especially useful for first-time contributors.

…ttps unless the request explicitly opts in (local-scene development)

## Problem

`InvalidOperationException: Insecure connection not allowed` on every wearable main-file /
profile / content fetch whenever the request URL scheme is `http`. Sentry
UNITY-EXPLORER-P42 (105 ev/wk) + P4G (76 ev/wk), ongoing since May. Every sampled event is
the Creator Hub / sdk-commands local preview flow (`http://127.0.0.1:8000`): release builds
systematically break the officially supported local-preview path for every creator.

## Root cause

The client's transport-security policy lives solely in Unity's baked, global
`insecureHttpOption: NotAllowed`, which cannot express the policy the product needs —
cleartext is fine to loopback (local preview, sidecars), never to real hosts. Not a
wearables bug: the wearable frame is just the highest-volume victim of the missing scheme
policy at the web-request choke point.

## Fix

1. `ProjectSettings.asset`: `insecureHttpOption` → AlwaysAllowed; enforcement moves into
   the client layer.
2. `WebRequestUtils`: pure static policy — `EnforceSecureScheme(url)` +
   `IsForbiddenCleartext(url)`. Non-http schemes untouched (zero-alloc https fast path);
   http+loopback (`localhost`, 127.0.0.0/8, `[::1]` via `IPAddress.IsLoopback`) allowed;
   http+any other host rewritten to https (those threw before sending at the pin, so the
   upgrade can only equal or improve the outcome). Unparsable http URLs fail closed.
3. `RequestEnvelope.InitializedWebRequest`: secure-scheme enforcement applies to the wire
   URL of **every request, signed and unsigned alike** — default-deny is the property that
   justifies flipping the player setting to AlwaysAllowed. The single exemption is an
   explicit per-request opt-in: `CommonArguments.AllowInsecureCleartext` (new readonly
   field, trailing optional ctor param, default false), set only by the scene `fetch()`
   API (`SimpleFetchApiImplementation.FetchAsync`) in local-scene-development mode. That
   module already enforces its own dev-mode-aware gate — https required unless
   `isLocalSceneDevelopment` — and in dev mode a scene may fetch cleartext http to any
   host (LAN dev server, remote test endpoint), so the envelope must not rewrite those.
   Enforcement runs before signing, so a signed request's signature covers the upgraded
   URL and the identity auth chain never travels over forbidden cleartext; loopback http
   always passes through, so signed requests against a local preview realm keep working.
   Per-request composition (the media-converter's embedded http origin query param) is
   untouched. One warning log per upgrade for support triage.
4. `WebRequestController.SendAsync`: redirect-downgrade guard —
   `IsCleartextDowngrade(sentUrl, finalUrl)` fires only when the exchange left on an
   allowed scheme/host (https, or loopback http) and its final (post-redirect) URL is
   forbidden cleartext; checked on both success and exception paths, so a downgraded
   exchange is never consumed and never retried. Because every non-opted-in wire URL is
   upgraded to https before send, the guard covers all of that traffic; an exchange
   deliberately sent as cleartext (an opted-in local-scene-development fetch) is not a
   downgrade and is not blocked.
   This is weaker than the old player setting's pre-send block —
   Unity's internal redirect handling has already sent the downgraded hop by the time the
   guard sees it; a true per-hop block would need `redirectLimit = 0` + manual redirect
   handling for signed requests (follow-up). The guard throws a distinct
   "Insecure redirect blocked" message so these events don't land in the P42/P4G buckets.
5. `WebRequestUtils.IsLocalhost`: reimplemented on top of the policy's single loopback
   definition (`Uri`-parsed; http/https + `localhost`/127.0.0.0/8/`[::1]`) — the old
   prefix check missed 127.0.0.0/8 beyond 127.0.0.1 (rerouting loopback textures through
   the public media converter, which cannot reach the creator's machine) and
   false-positived on `localhost.evil.com`.
6. Bypass sites the settings flip would otherwise leave un-gated now apply the same policy:
   `UrlResolverService` (scene-controlled media URLs — the resolved URL itself is upgraded,
   so reachability probes and playback agree on one wire URL, and the raw GET probe
   enforces the scheme at the emit site) and `AbgenSidecar` (the realm root behind its
   /about + catalyst fetches; loopback LSD preview roots pass through unchanged).

Known behavior change: http realms/worlds (#7827 class) now fail as a retried TLS connect
error instead of a synchronous "Insecure connection not allowed" — same terminal outcome,
different message, slower on probe-style paths.

## Test

New EditMode `InsecureSchemePolicyShould` (40 cases): default-deny locks
(`UpgradeNonLoopbackHttpToHttpsWhenUnsigned` ×2 and `UpgradeNonConvertedTextureUrlAtTheWire`
— unsigned envelope/texture wire URLs are upgraded to https), the dev opt-in lock
(`PassNonLoopbackHttpThroughUnchangedWhenCleartextAllowed` ×2 — with
`CommonArguments.AllowInsecureCleartext` the URL comes out byte-identical),
signed-request upgrade (enforce-before-sign) + signed-loopback pass-through, the
`KeepPlayerSettingAlwaysAllowed` lock pairing the settings flip with the client policy,
loopback pass-through combinatorial over the opt-in flag (never upgraded either way),
https/file pass-through guards, the converter-embedded-origin preservation guard,
`IsForbiddenCleartext` + `IsCleartextDowngrade` classification cases (downgrade,
deliberate-cleartext, redirect-to-loopback), the loopback-texture-stays-direct guard (ktx
enabled), and `IsLocalhost` classification cases (127.0.0.0/8, `localhost.evil.com`).
Plus 3 `UrlResolverServiceShould` cases: direct non-loopback http upgraded for probe +
playback, loopback http kept cleartext.

## Validation

Windows Unity 6000.4 EditMode lane at the pin: RED FAIL 3 as intended (non-loopback http
stayed http; setting was NotAllowed) + 7 guards passed / GREEN PASS 16/16 for the original
round. Rescope round (security review): GREEN (full fix) passes all 40; RED (same tests
with the envelope guard reverted to signed-only) fails exactly the 3 default-deny locks —
the coverage the review flagged as dropped.

Fixes #3661
Related: #8761, #8762 (same-day auto-closed dupes of P42/P4G), #8080 (MediaStream direct
probe — its bypass site now applies the same policy, see Fix 6),
#7827 (realm-connect variant; failure message changes as described above)

Includes inspection-warning cleanup in all touched files.
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

🚦 CI Status

Build

New build in progress, come back later!

Lint

No C# files changed — lint ratchet skipped.

Tests

Waiting for tests to start…

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.

System.InvalidOperationException: Insecure connection not allowed

3 participants