fix(mgmt): re-assert MxGateway protocol at secured-write execute (D2 TOCTOU guard, T14b)

This commit is contained in:
Joseph Doherty
2026-06-18 04:24:53 -04:00
parent 40928535fd
commit 0c7774acdc
2 changed files with 104 additions and 0 deletions
@@ -1045,6 +1045,35 @@ public class ManagementActor : ReceiveActor
await EmitSecuredWriteAuditAsync(
sp, AuditKind.SecuredWriteApprove, AuditStatus.Submitted, row, actor: user.Username);
// D2 / T14 TOCTOU guard: re-assert the MxGateway protocol AT EXECUTE, not only at
// submit. The connection named on the row may have been reconfigured/recreated as a
// non-MxGateway (e.g. OPC UA) connection between submit and approval; relaying then
// would execute the secured write against a non-MxGateway adapter, violating the
// feature's core safety invariant. Re-load the connection with the same lookup
// submit uses (resolve site -> its data connections -> match by name) and fail the
// row deterministically (mirrors the decode-error containment below) if it is
// missing or no longer MxGateway — WITHOUT relaying.
var siteRepo = sp.GetRequiredService<ISiteRepository>();
var execSite = await siteRepo.GetSiteByIdentifierAsync(row.SiteId);
var execConn = execSite is null
? null
: (await siteRepo.GetDataConnectionsBySiteIdAsync(execSite.Id))
.FirstOrDefault(c => string.Equals(c.Name, row.ConnectionName, StringComparison.Ordinal));
if (execConn is null ||
!string.Equals(execConn.Protocol, "MxGateway", StringComparison.OrdinalIgnoreCase))
{
row.Status = "Failed";
row.ExecutedAtUtc = DateTime.UtcNow;
row.ExecutionError = execConn is null
? $"connection '{row.ConnectionName}' not found"
: $"connection '{row.ConnectionName}' is no longer an MxGateway connection";
await repo.UpdateAsync(row);
await EmitSecuredWriteAuditAsync(
sp, AuditKind.SecuredWriteExecute, AuditStatus.Failed, row,
actor: user.Username, errorMessage: row.ExecutionError);
return ToSecuredWriteDto(row);
}
// Validate the value type BEFORE attempting the relay. An unknown type can
// never be decoded/written, so fail the row deterministically rather than
// leaving it stuck Approved. (Addresses the C2 reviewer's deferred