Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -13,18 +13,21 @@ namespace Global.AppArgs
/// <list type="bullet">
/// <item>
/// <b>Always permitted</b> — benign navigation / share / login intents whose worst case is already
/// gated elsewhere (a consent prompt, a matching login token, or a plain coordinate): realm,
/// position, community, signin, authRequestId, force-open-backpack, spawnpoint.
/// gated elsewhere (a consent prompt, a matching login token, a plain coordinate, or a closed
/// Decentraland-owned enum): realm, position, community, signin, authRequestId, force-open-backpack,
/// spawnpoint, dclenv.
/// </item>
/// <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
/// SDK (<c>sdk-commands</c>) attach to their preview deep links: local-scene, hub,
/// 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, 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 @@ -70,6 +73,14 @@ public static class DeepLinkAllowlist
// POSITION — it only picks where inside an already-permitted realm/position navigation the user arrives,
// with no capability, infra, or exec impact.
AppArgsFlags.SPAWN_POINT,

// Target environment (org|zone|today). Not realm-gated: the login callbacks and jump-in links that carry
// it have no realm at all, so the loopback-realm condition below could never pass for them and the session
// would silently fall back to the default environment. Safe on its own — a closed Decentraland-owned enum,
// parsed with Enum.TryParse where it is consumed and ignored when it does not match, never a URL, so it
// cannot point the client at attacker infrastructure. Worst case is a session in a Decentraland-owned test
// environment, strictly less capable than the attacker-supplied REALM above.
AppArgsFlags.ENVIRONMENT,
};

// Local-development params Creator Hub / sdk-commands attach to preview deep links. Permitted ONLY when the
Expand All @@ -82,9 +93,6 @@ public static class DeepLinkAllowlist
// local server; loopback-gated so an attacker can't point LSD at a remote realm (SEC-020).
AppArgsFlags.LOCAL_SCENE,

// Target environment (org/zone/today). A DCL-owned enum, not a URL — cannot point at attacker infra.
AppArgsFlags.ENVIRONMENT,

// Marks the session as launched from the Creator Hub (analytics trait only — no capability unlock).
AppArgsFlags.DCL_EDITOR,

Expand All @@ -110,6 +118,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 @@ -66,7 +66,7 @@ public void DeepLinkDropsInternalFlags()
public void DeepLinkKeepsAllowlistedParams()
{
Dictionary<string, string> output = ApplicationParametersParser.ProcessDeepLinkParameters(
"decentraland://?realm=https://peer.decentraland.org&position=10,20&community=abc&signin=id1&authRequestId=req1&force-open-backpack=true&spawnpoint=lobby");
"decentraland://?realm=https://peer.decentraland.org&position=10,20&community=abc&signin=id1&authRequestId=req1&force-open-backpack=true&spawnpoint=lobby&dclenv=zone");

Assert.AreEqual("https://peer.decentraland.org", output.GetValueOrDefault(AppArgsFlags.REALM));
Assert.AreEqual("10,20", output.GetValueOrDefault(AppArgsFlags.POSITION));
Expand All @@ -75,6 +75,22 @@ public void DeepLinkKeepsAllowlistedParams()
Assert.AreEqual("req1", output.GetValueOrDefault(AppArgsFlags.AUTH_REQUEST_ID));
Assert.IsTrue(output.ContainsKey(AppArgsFlags.FORCE_OPEN_BACKPACK), "force-open-backpack must survive (shipped feature #9398)");
Assert.AreEqual("lobby", output.GetValueOrDefault(AppArgsFlags.SPAWN_POINT), "spawnpoint must survive (named scene spawn point #9369)");
Assert.AreEqual("zone", output.GetValueOrDefault(AppArgsFlags.ENVIRONMENT), "dclenv must survive for any realm: it is the only channel that carries the environment into a launched client");
}

[Test]
public void DeepLinkKeepsEnvironmentForRealmlessLoginCallback()
{
Dictionary<string, string> output = ApplicationParametersParser.ProcessDeepLinkParameters(
"decentraland://open?signin=id1&dclenv=zone&authRequestId=req1&local-scene=true&skip-auth-screen=true&multi-instance=true&scene-console=true");

Assert.AreEqual("zone", output.GetValueOrDefault(AppArgsFlags.ENVIRONMENT), "dclenv must survive a realm-less login callback, otherwise the client falls back to the default environment");
Assert.AreEqual("id1", output.GetValueOrDefault(AppArgsFlags.SIGNIN));
Assert.AreEqual("req1", output.GetValueOrDefault(AppArgsFlags.AUTH_REQUEST_ID));
Assert.IsFalse(output.ContainsKey(AppArgsFlags.LOCAL_SCENE), "local-scene must still require a loopback realm");
Assert.IsFalse(output.ContainsKey(AppArgsFlags.SKIP_AUTH_SCREEN), "skip-auth-screen must still require a loopback realm");
Assert.IsFalse(output.ContainsKey(AppArgsFlags.MULTIPLE_RUNNING_INSTANCES), "multi-instance must still require a loopback realm");
Assert.IsFalse(output.ContainsKey(AppArgsFlags.SCENE_CONSOLE), "scene-console must stay dropped for every realm");
}

[Test]
Expand All @@ -99,10 +115,9 @@ public void DeepLinkDropsLocalSceneForRemoteRealm()
public void DeepLinkKeepsSdkAndCreatorHubDevParamsForLoopbackRealm()
{
Dictionary<string, string> output = ApplicationParametersParser.ProcessDeepLinkParameters(
"decentraland://?realm=http://127.0.0.1:8000&position=10,20&local-scene=true&dclenv=zone&hub=true&skip-auth-screen=true&landscape-terrain-enabled=true&multi-instance=true");
"decentraland://?realm=http://127.0.0.1:8000&position=10,20&local-scene=true&hub=true&skip-auth-screen=true&landscape-terrain-enabled=true&multi-instance=true");

Assert.AreEqual("true", output.GetValueOrDefault(AppArgsFlags.LOCAL_SCENE), "local-scene");
Assert.AreEqual("zone", output.GetValueOrDefault(AppArgsFlags.ENVIRONMENT), "dclenv");
Assert.AreEqual("true", output.GetValueOrDefault(AppArgsFlags.DCL_EDITOR), "hub");
Assert.AreEqual("true", output.GetValueOrDefault(AppArgsFlags.SKIP_AUTH_SCREEN), "skip-auth-screen");
Assert.AreEqual("true", output.GetValueOrDefault(AppArgsFlags.LANDSCAPE_TERRAIN_ENABLED), "landscape-terrain-enabled");
Expand All @@ -113,10 +128,9 @@ public void DeepLinkKeepsSdkAndCreatorHubDevParamsForLoopbackRealm()
public void DeepLinkDropsSdkAndCreatorHubDevParamsForRemoteRealm()
{
Dictionary<string, string> output = ApplicationParametersParser.ProcessDeepLinkParameters(
"decentraland://?realm=https://peer.decentraland.org&local-scene=true&dclenv=zone&hub=true&skip-auth-screen=true&landscape-terrain-enabled=true&multi-instance=true");
"decentraland://?realm=https://peer.decentraland.org&local-scene=true&hub=true&skip-auth-screen=true&landscape-terrain-enabled=true&multi-instance=true");

Assert.IsFalse(output.ContainsKey(AppArgsFlags.LOCAL_SCENE), "local-scene must be dropped for a remote realm");
Assert.IsFalse(output.ContainsKey(AppArgsFlags.ENVIRONMENT), "dclenv must be dropped for a remote realm");
Assert.IsFalse(output.ContainsKey(AppArgsFlags.DCL_EDITOR), "hub must be dropped for a remote realm");
Assert.IsFalse(output.ContainsKey(AppArgsFlags.SKIP_AUTH_SCREEN), "skip-auth-screen must be dropped for a remote realm");
Assert.IsFalse(output.ContainsKey(AppArgsFlags.LANDSCAPE_TERRAIN_ENABLED), "landscape-terrain-enabled must be dropped for a remote realm");
Expand All @@ -127,11 +141,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 +177,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
Loading
Loading