fix(security): env: credential references follow the per-instance rule (#849)
env:<PREFIX><INSTANCE>_... is the per-instance convention for every prefix of a driver's scope (OTOPCUA_GALAXY_, GALAXY_MXGW_, OTOPCUA_OPCUACLIENT_, OTOPCUA_HEIDENHAIN_), INSTANCE = a driver id or name upper-cased with every other character as '_'. A variable whose part after the prefix equals or starts with (at a '_' boundary) ANOTHER same-type instance's id or name is refused with CredentialReferenceOtherInstance at save, deploy, Test Connect and browse - the same DriverCredentialPolicy.DescribeOtherInstanceProblems the secret: rule uses (now over CollectInstanceReferences / ReferenceNames). Another-instance matches win over the owner's, as for secrets. Shared variables that name no instance (GALAXY_MXGW_API_KEY) keep working. HasInstanceSegments now also gates on env:, so an env:-only browse needs the ConfigDb (fail-closed without it). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+35
@@ -72,6 +72,41 @@ public sealed class BrowserSessionCredentialScopeTests
|
||||
universal.Opens.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>lmxopcua#849: an env: reference named after ANOTHER instance never opens a browser either.</summary>
|
||||
[Theory]
|
||||
[InlineData("env:OTOPCUA_GALAXY_VICTIM_KEY")]
|
||||
[InlineData("env:GALAXY_MXGW_DRV_VICTIM_KEY")]
|
||||
public async Task An_env_reference_naming_another_instance_never_opens_a_browser(string reference)
|
||||
{
|
||||
var bespoke = new RecordingBrowser(DriverTypeNames.Galaxy);
|
||||
var svc = new BrowserSessionService(
|
||||
[bespoke], new BrowseSessionRegistry(), NullLogger<BrowserSessionService>.Instance, new RecordingUniversal(), null!, null!,
|
||||
SeededFactory());
|
||||
|
||||
var result = await svc.OpenAsync(DriverTypeNames.Galaxy,
|
||||
"""{"Gateway":{"Endpoint":"http://evil:5001","ApiKeySecretRef":"REF"}}""".Replace("REF", reference), "DRV-evil",
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
result.Ok.ShouldBeFalse();
|
||||
result.Message.ShouldNotBeNull().ShouldContain("lmxopcua#849");
|
||||
bespoke.Opens.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>Positive control: a shared env variable that names no instance reaches the browser.</summary>
|
||||
[Fact]
|
||||
public async Task A_shared_env_variable_reaches_the_browser()
|
||||
{
|
||||
var bespoke = new RecordingBrowser(DriverTypeNames.Galaxy);
|
||||
var svc = new BrowserSessionService(
|
||||
[bespoke], new BrowseSessionRegistry(), NullLogger<BrowserSessionService>.Instance, new RecordingUniversal(), null!, null!,
|
||||
SeededFactory());
|
||||
|
||||
await svc.OpenAsync(DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":"env:GALAXY_MXGW_API_KEY"}}""",
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
bespoke.Opens.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Positive control: the owning driver's own id-scoped secret reaches the browser.</summary>
|
||||
[Fact]
|
||||
public async Task The_owners_own_secret_reaches_the_browser()
|
||||
|
||||
+38
@@ -76,6 +76,44 @@ public sealed class AdminOperationsActorCredentialScopeTests : ControlPlaneActor
|
||||
probe.Calls.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>lmxopcua#849: an env: reference named after ANOTHER instance never reaches the probe either.</summary>
|
||||
[Theory]
|
||||
[InlineData("env:OTOPCUA_GALAXY_VICTIM_API_KEY")]
|
||||
[InlineData("env:GALAXY_MXGW_DRV_VICTIM_KEY")]
|
||||
public void An_env_reference_naming_another_instance_never_reaches_the_probe(string reference)
|
||||
{
|
||||
var factory = NewInMemoryDbFactory();
|
||||
SeedGalaxyDrivers(factory);
|
||||
var probe = new RecordingProbe(DriverTypeNames.Galaxy);
|
||||
var actor = Sys.ActorOf(AdminOperationsActor.Props(factory, CreateTestProbe("coord").Ref, new IDriverProbe[] { probe }));
|
||||
|
||||
actor.Tell(new TestDriverConnect(
|
||||
DriverTypeNames.Galaxy, """{"Gateway":{"Endpoint":"http://attacker:5001","ApiKeySecretRef":"REF"}}""".Replace("REF", reference),
|
||||
5, Guid.NewGuid(), "DRV-evil"));
|
||||
|
||||
var reply = ExpectMsg<TestDriverConnectResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
|
||||
reply.Ok.ShouldBeFalse();
|
||||
reply.Message.ShouldNotBeNull().ShouldContain("lmxopcua#849");
|
||||
probe.Calls.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>Positive control: a shared env variable naming no instance reaches the probe.</summary>
|
||||
[Fact]
|
||||
public void A_shared_env_variable_reaches_the_probe()
|
||||
{
|
||||
var factory = NewInMemoryDbFactory();
|
||||
SeedGalaxyDrivers(factory);
|
||||
var probe = new RecordingProbe(DriverTypeNames.Galaxy);
|
||||
var actor = Sys.ActorOf(AdminOperationsActor.Props(factory, CreateTestProbe("coord").Ref, new IDriverProbe[] { probe }));
|
||||
|
||||
actor.Tell(new TestDriverConnect(
|
||||
DriverTypeNames.Galaxy, """{"Gateway":{"Endpoint":"http://attacker:5001","ApiKeySecretRef":"env:GALAXY_MXGW_API_KEY"}}""",
|
||||
5, Guid.NewGuid()));
|
||||
|
||||
ExpectMsg<TestDriverConnectResult>(PresenceBudget, cancellationToken: TestContext.Current.CancellationToken).Ok.ShouldBeTrue();
|
||||
probe.Calls.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Positive control: the owner's own secret (and a shared one) does reach the probe.</summary>
|
||||
[Theory]
|
||||
[InlineData("secret:galaxy/DRV-victim/apikey")]
|
||||
|
||||
Reference in New Issue
Block a user