From 5ba769e90aa3c30431c01e08a1c60f8726de2b31 Mon Sep 17 00:00:00 2001 From: Strycher Date: Mon, 22 Jun 2026 02:32:29 -0400 Subject: [PATCH] fix(#72): harden codec -- bound BrokerListDecoder + validate slot range (C1) Greens A1's adversarial tests, fixing the Gemini findings: - BLOCKER (DoS): BrokerListDecoder drops out-of-range slots and caps total retained fields (brokerSlotCount*32), so a hostile BROKER_KV flood cannot grow memory without bound. - MINOR: parse() rejects broker slots >= brokerSlotCount (was: stored slot 200). Co-Authored-By: Claude Opus 4.8 --- lib/connector/observer_config_client.dart | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/connector/observer_config_client.dart b/lib/connector/observer_config_client.dart index 7238469..8934946 100644 --- a/lib/connector/observer_config_client.dart +++ b/lib/connector/observer_config_client.dart @@ -214,7 +214,10 @@ class ObserverConfigClient { case rBrokersStart: return ConfigBrokersStart(f.length > 2 ? f[2] : 0); case rBrokerKv: - if (f.length < 3) return const ConfigUnknown(rBrokerKv); + // Reject out-of-range slots from a hostile device (contract: 0..N-1). + if (f.length < 3 || f[2] >= brokerSlotCount) { + return const ConfigUnknown(rBrokerKv); + } final body = _text(f, 3); final eq = body.indexOf( '=', @@ -261,8 +264,14 @@ class ObserverConfigClient { /// [BrokerConfig]s. Feed each parsed [ConfigResponse]; [add] returns the /// decoded list once END arrives (or `[]` for an empty pool), else null. class BrokerListDecoder { + /// Hard cap on total key/value lines retained across the whole dump — a + /// hostile device cannot grow memory without bound (Gemini BLOCKER: DoS). + /// Generous for the 10-slot contract (~16 fields each). + static const int _maxFields = ObserverConfigClient.brokerSlotCount * 32; + final Map> _slots = {}; bool _open = false; + int _fields = 0; /// Feed one response. Returns the broker list on END, else null. /// Anything before START or after END is ignored. @@ -270,10 +279,18 @@ class BrokerListDecoder { switch (r) { case ConfigBrokersStart(): _slots.clear(); + _fields = 0; _open = true; return null; case ConfigBrokerKv(:final slot, :final key, :final value): - if (_open) (_slots[slot] ??= {})[key] = value; + // Drop out-of-range slots and stop accumulating past the field cap. + if (_open && + slot >= 0 && + slot < ObserverConfigClient.brokerSlotCount && + _fields < _maxFields) { + (_slots[slot] ??= {})[key] = value; + _fields++; + } return null; case ConfigBrokersEnd(): if (!_open) return null;