Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ public static class AppArgsFlags
public const string LSD_USE_REMOTE_AB = "lsd-use-remote-ab";
public const string LSD_REMOTE_AB_SERVER = "lsd-remote-ab-server";
public const string LSD_REMOTE_AB_WORLD = "lsd-remote-ab-world";
/// <summary>
/// Local scene development only: load assets as asset bundles served by the preview server at
/// {realm}/optimized-assets instead of raw GLTFs. Carries no URL or port — the base is derived
/// from the realm the client already has.
/// </summary>
public const string LOCAL_AB = "local-ab";

public const string OPTIMIZED_ASSETS_URL = "optimized-assets-url";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,9 @@ public static Dictionary<string, string> ProcessDeepLinkParameters(string deepLi
}

// Tier 2 (SEC-019/020): the local-development params Creator Hub / sdk-commands attach to preview deep
// links (local-scene, dclenv, hub, skip-auth-screen, landscape-terrain-enabled, multi-instance) are
// permitted only when the target realm is loopback — a remote-realm deep link from a web page cannot
// enable them. Everything not in either tier is dropped.
// links (DeepLinkAllowlist.LOOPBACK_REALM_PERMITTED_KEYS, with per-key rationale) are permitted only
// when the target realm is loopback — a remote-realm deep link from a web page cannot enable them.
// Everything not in either tier is dropped.
bool realmIsLoopback = output.TryGetValue(AppArgsFlags.REALM, out string? loopbackRealm)
&& Uri.TryCreate(loopbackRealm, UriKind.Absolute, out Uri? loopbackRealmUri)
&& loopbackRealmUri.IsLoopback;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ namespace Global.AppArgs
/// <item>
/// <b>Permitted only for a loopback realm</b> — the local-development params Creator Hub and the
/// SDK (<c>sdk-commands</c>) attach to their preview deep links: local-scene, dclenv, hub,
/// skip-auth-screen, landscape-terrain-enabled, multi-instance, mcp, mcp-port. They are gated on
/// skip-auth-screen, landscape-terrain-enabled, multi-instance, mcp, mcp-port, local-ab.
/// They are gated on
/// <c>Uri.IsLoopback</c> of the target realm (127.0.0.1 / localhost / [::1]) so a remote-realm deep
/// link from a web page can never enable them, while a legitimate local-dev launch (which always
/// targets loopback) works. All but the MCP pair are individually low-harm — an analytics tag, a
/// cosmetic toggle, an instance count, an env enum, or a screen skip that still forces auth when no
/// valid identity is cached; <c>mcp</c>/<c>mcp-port</c> start an unauthenticated loopback control
/// cosmetic toggle, an instance count, an env enum, a screen skip that still forces auth when no
/// valid identity is cached, or an asset-server toggle whose base URL derives from the realm this
/// gate already checked; <c>mcp</c>/<c>mcp-port</c> start an unauthenticated loopback control
/// port, so they lean on the gate plus the server's own 127.0.0.1 bind and Origin check — see the
/// per-key comment for what the gate does and does not cover.
/// </item>
Expand Down Expand Up @@ -110,6 +112,15 @@ public static class DeepLinkAllowlist
// Port the server above listens on. Presence alone also starts it (MCP_PORT implies MCP), so it carries
// the same gate; the value is clamped to 1024-65535 and falls back to the default port (McpServerPlugin).
AppArgsFlags.MCP_PORT,

// Local-scene development only: load the scene's asset bundles from the preview server instead of raw
// GLTFs. A pure boolean — the optimized-assets base is derived from the realm itself
// ({realm}/optimized-assets, see RealmLaunchSettings.LocalAssetBundlesBaseUrl), the same value this
// gate already requires to be loopback, so the flag adds no attacker-controllable input: it can only
// point asset loading at the realm the link already targets. The full-URL variant
// (optimized-assets-url) points AB/LOD/registry endpoints at arbitrary infrastructure and stays
// never-permitted.
AppArgsFlags.LOCAL_AB,
};

public static bool IsPermitted(string key) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,12 @@ public void DeepLinkDropsSdkAndCreatorHubDevParamsForRemoteRealm()
public void DeepLinkDropsExecAndInfraParamsEvenForLoopbackRealm()
{
Dictionary<string, string> output = ApplicationParametersParser.ProcessDeepLinkParameters(
"decentraland://?realm=http://127.0.0.1:8000&creator-hub-bin-path=x&launch-cdp-monitor-on-start=true&comms-adapter=y");
"decentraland://?realm=http://127.0.0.1:8000&creator-hub-bin-path=x&launch-cdp-monitor-on-start=true&comms-adapter=y&optimized-assets-url=https://evil.example");

Assert.IsFalse(output.ContainsKey("creator-hub-bin-path"), "creator-hub-bin-path must never be permitted (SEC-005), even for a loopback realm");
Assert.IsFalse(output.ContainsKey(AppArgsFlags.LAUNCH_CDP_MONITOR_ON_START), "launch-cdp-monitor-on-start must never be permitted, even for a loopback realm");
Assert.IsFalse(output.ContainsKey(AppArgsFlags.COMMS_ADAPTER), "comms-adapter must never be permitted, even for a loopback realm");
Assert.IsFalse(output.ContainsKey(AppArgsFlags.OPTIMIZED_ASSETS_URL), "optimized-assets-url must never be permitted, even for a loopback realm — it points the AB/LOD/registry endpoints at arbitrary infrastructure for the whole session; local-ab derives the base from the realm instead");
}

[Test]
Expand Down Expand Up @@ -162,5 +163,23 @@ public void DeepLinkDropsMcpWithoutRealm()
Assert.IsFalse(output.ContainsKey(AppArgsFlags.MCP), "mcp must be dropped when the link carries no realm at all (drive-by link against the default realm)");
Assert.IsFalse(output.ContainsKey(AppArgsFlags.MCP_PORT), "mcp-port must be dropped when the link carries no realm at all");
}

[Test]
public void DeepLinkKeepsLocalAbForLoopbackRealm()
{
Dictionary<string, string> output = ApplicationParametersParser.ProcessDeepLinkParameters(
"decentraland://?realm=http://127.0.0.1:8000&local-scene=true&local-ab=true");

Assert.AreEqual("true", output.GetValueOrDefault(AppArgsFlags.LOCAL_AB), "local-ab must survive for a loopback (local dev) realm — Creator Hub forwards it into the preview deep link");
}

[Test]
public void DeepLinkDropsLocalAbForRemoteRealm()
{
Dictionary<string, string> output = ApplicationParametersParser.ProcessDeepLinkParameters(
"decentraland://?realm=https://peer.decentraland.org&local-scene=true&local-ab=true");

Assert.IsFalse(output.ContainsKey(AppArgsFlags.LOCAL_AB), "local-ab must be dropped for a non-loopback (remote) realm");
}
}
Comment thread
dalkia marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ private async UniTask InitializeFlowAsync(CancellationToken ct)
applicationParametersParser.TryGetValue(AppArgsFlags.OPTIMIZED_ASSETS_URL, out string? cliOptimizedAssetsUrl);

if (string.IsNullOrEmpty(cliOptimizedAssetsUrl) && launchSettings.useLocalAssetBundles)
cliOptimizedAssetsUrl = RealmLaunchSettings.DEFAULT_LOCAL_ASSET_BUNDLES_URL;
cliOptimizedAssetsUrl = launchSettings.LocalAssetBundlesBaseUrl();

var decentralandUrlsSource = new GatewayUrlsSource(
decentralandEnvironment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ namespace Global.Dynamic
public class RealmLaunchSettings : ILaunchMode
{
/// <summary>
/// abgen's default port; used for local asset bundles when no explicit --optimized-assets-url is given.
/// Path under the local-scene-development realm where the preview server proxies its abgen sidecar
/// (decentraland/js-sdk-toolchain#1504) — the client derives the optimized-assets base from the realm
/// origin it already has instead of being told a second origin.
/// </summary>
public const string DEFAULT_LOCAL_ASSET_BUNDLES_URL = "http://127.0.0.1:5147";
public const string OPTIMIZED_ASSETS_PATH = "/optimized-assets";

[Serializable]
public struct PredefinedScenes
Expand All @@ -41,9 +43,9 @@ public struct PredefinedScenes
[SerializeField] internal string remoteHibridWorld = "MetadyneLabs.dcl.eth";
[SerializeField] internal HybridSceneContentServer remoteHybridSceneContentServer = HybridSceneContentServer.Goerli;
[SerializeField] internal bool useRemoteAssetsBundles;
[SerializeField] [Tooltip("Local scene development only: load the scene's asset bundles from a locally running abgen instead of loading "
+ "raw GLTFs. The server URL comes from --optimized-assets-url, defaulting to " + DEFAULT_LOCAL_ASSET_BUNDLES_URL
+ " (abgen's default port) when not provided")] internal bool useLocalAssetBundles;
[SerializeField] [Tooltip("Local scene development only: load the scene's asset bundles from the preview server instead of loading "
+ "raw GLTFs. The assets base is derived from the realm ({realm}" + OPTIMIZED_ASSETS_PATH + "); "
+ "an explicit --optimized-assets-url overrides it")] internal bool useLocalAssetBundles;
[SerializeField] [Tooltip("In Worlds there is one LiveKit room for all scenes so it's possible to communicate changes outside of the scene. "
+ "In Genesis City there are individual LiveKit rooms and only one connection at a time is maintained. "
+ "Toggle this flag to equalize this behavior")] internal bool isolateSceneCommunication;
Expand Down Expand Up @@ -96,6 +98,23 @@ public HybridSceneParams CreateHybridSceneParams()
return new HybridSceneParams();
}

/// <summary>
/// Base URL for local asset bundles: the local-scene-development realm plus
/// <see cref="OPTIMIZED_ASSETS_PATH" />. No value other than the realm itself — which the deep-link
/// allowlist already gates on being loopback — feeds the result, so a deep link cannot point it
/// anywhere the realm doesn't already reach. Null outside local scene development.
/// </summary>
public string? LocalAssetBundlesBaseUrl()
{
if (isLocalSceneDevelopmentRealm)
return customRealm.TrimEnd('/') + OPTIMIZED_ASSETS_PATH;

if (initialRealm == InitialRealm.Localhost)
return IRealmNavigator.LOCALHOST + OPTIMIZED_ASSETS_PATH;

return null;
}

public void ApplyConfig(IAppArgs applicationParameters)
{
if (applicationParameters.TryGetValue(AppArgsFlags.REALM, out string? realm))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,46 @@ public void IgnoreWindowsRealmInvalidation(string world)
Assert.AreEqual(world, realmLaunchSettings.TargetWorld);
}

[TestCase("http://127.0.0.1:8000", "http://127.0.0.1:8000/optimized-assets")]
[TestCase("http://127.0.0.1:8000/", "http://127.0.0.1:8000/optimized-assets")] // trailing slash must not double up
[TestCase("http://localhost:8001", "http://localhost:8001/optimized-assets")]
public void DeriveLocalAssetBundlesBaseUrlFromRealm(string realm, string expectedUrl)
{
//Arrange
var realmLaunchSettings = new RealmLaunchSettings();

ApplicationParametersParser applicationParametersParser = new (new[]
{
$"decentraland://?realm={realm}&position=100,100&local-scene=true&local-ab=true",
});

//Act
realmLaunchSettings.ApplyConfig(applicationParametersParser);

//Assert
Assert.IsTrue(realmLaunchSettings.useLocalAssetBundles, "local-ab must enable local asset bundles");
Assert.AreEqual(expectedUrl, realmLaunchSettings.LocalAssetBundlesBaseUrl());
}

[Test]
public void NotDeriveLocalAssetBundlesBaseUrlOutsideLocalSceneDevelopment()
{
//Arrange
var realmLaunchSettings = new RealmLaunchSettings();

ApplicationParametersParser applicationParametersParser = new (new[]
{
"--realm",
"https://peer.decentraland.org",
});

//Act
realmLaunchSettings.ApplyConfig(applicationParametersParser);

//Assert
Assert.IsNull(realmLaunchSettings.LocalAssetBundlesBaseUrl());
}

[Test]
[TestCase("127.0.0.1:8000")]
[TestCase("localhost:8000")]
Expand Down
Loading