fix(siteruntime): reject SetStaticAttribute with malformed list value (no silent poison persist)

This commit is contained in:
Joseph Doherty
2026-06-16 15:59:30 -04:00
parent 7f97780bb3
commit ad6bfc8af9
2 changed files with 115 additions and 1 deletions
@@ -344,7 +344,33 @@ public class InstanceActor : ReceiveActor
if (_resolvedAttributeByName.TryGetValue(command.AttributeName, out var resolved)
&& IsListAttribute(resolved))
{
_attributes[command.AttributeName] = DecodeAttributeValue(resolved, command.Value);
// MV-7: the script path pre-encodes valid canonical JSON via ScopeAccessors,
// but the Inbound API / direct-command path can submit an arbitrary
// command.Value. A non-empty value that fails to decode (malformed JSON,
// bad element, missing element type) is poison: storing it would null the
// in-memory value yet publish "Good" quality and durably persist the bad
// JSON (which then loads as Bad next restart). Reject such writes outright.
// Note: DecodeAttributeValue returns null for BOTH a null/empty input
// (valid — clearing) AND a malformed non-empty input (invalid). Only the
// latter is rejected, hence the explicit IsNullOrWhiteSpace guard. An empty
// list "[]" decodes to a non-null empty List, so it passes through.
var decoded = DecodeAttributeValue(resolved, command.Value);
if (!string.IsNullOrWhiteSpace(command.Value) && decoded == null)
{
_logger.LogWarning(
"SetAttribute rejected — value for List attribute '{Attribute}' on instance '{Instance}' is not a valid list",
command.AttributeName, _instanceUniqueName);
Sender.Tell(new SetStaticAttributeResponse(
command.CorrelationId,
_instanceUniqueName,
command.AttributeName,
false,
$"Invalid list value for attribute '{command.AttributeName}'",
DateTimeOffset.UtcNow));
return;
}
_attributes[command.AttributeName] = decoded;
}
else
{