Found by the security review in the 2026-07 maintenance pass (#1643).
The defect
ReconcileEndpointFirewallAsync (Darling/PerformanceMonitor.Darling.Service/DarlingCliCommands.cs:1628) reads config.Web.Network?.AllowFrom (or the MCP equivalent) straight from darling.json and passes it to BuildFirewallEnableCommand (DarlingManagedPostgres.cs:1652-1654), which interpolates it unquoted into a PowerShell -Command string:
New-NetFirewallRule -DisplayName '<rule>' -Direction Inbound -Action Allow -Protocol TCP -LocalPort <port> -RemoteAddress <allowFrom> | Out-Null
The only check is string.IsNullOrWhiteSpace(allowFrom) at line 1617.
This is the one BuildFirewallEnableCommand caller that never parses the value as a CIDR first. Every other caller passes a canonicalized IPNetwork.ToString():
DarlingHostBinding.cs:213 (via DarlingWebHostService.cs:317)
DarlingManagedPostgres.cs:1083 and :1111
and the --configure-network wizard validates through ResolveWebBind before writing. ToggleEndpointAsync calls only DarlingConfig.Load, which parses JSON and never calls Validate(), so nothing upstream catches it either.
Failure scenario
A darling.json containing:
"web": { "network": { "listen": "10.0.0.5", "allowFrom": "10.0.0.0/24; <attacker command>" } }
makes --enable-web (or --enable-mcp) execute the attacker's command.
- Elevated shell (the documented way to run these verbs):
ClassifyFirewallPlan returns RunElevated and RunFirewallCommandAsync executes it as administrator.
- Non-elevated: the code prints the fully-injected command and instructs the operator to paste it into an elevated PowerShell — same outcome, via the human.
Whoever can write darling.json — or set DARLING_CONFIG, honored at DarlingConfig.cs:134-137 with no path restriction — gets code execution in the elevating operator's context.
Fix
Two layers, both cheap:
- Parse before use in
ReconcileEndpointFirewallAsync, matching every other call site:
if (!IPNetwork.TryParse(allowFrom.Trim(), out var cidr)) { /* refuse, do not touch the firewall */ } then pass cidr.ToString().
- Single-quote and escape the value inside
BuildFirewallEnableCommand (PowerShell escapes ' by doubling) so the builder is safe independent of its callers.
Plus a test pinning that a non-CIDR allowFrom never reaches a firewall command.
Found by the security review in the 2026-07 maintenance pass (#1643).
The defect
ReconcileEndpointFirewallAsync(Darling/PerformanceMonitor.Darling.Service/DarlingCliCommands.cs:1628) readsconfig.Web.Network?.AllowFrom(or the MCP equivalent) straight fromdarling.jsonand passes it toBuildFirewallEnableCommand(DarlingManagedPostgres.cs:1652-1654), which interpolates it unquoted into a PowerShell-Commandstring:The only check is
string.IsNullOrWhiteSpace(allowFrom)at line 1617.This is the one
BuildFirewallEnableCommandcaller that never parses the value as a CIDR first. Every other caller passes a canonicalizedIPNetwork.ToString():DarlingHostBinding.cs:213(viaDarlingWebHostService.cs:317)DarlingManagedPostgres.cs:1083and:1111and the
--configure-networkwizard validates throughResolveWebBindbefore writing.ToggleEndpointAsynccalls onlyDarlingConfig.Load, which parses JSON and never callsValidate(), so nothing upstream catches it either.Failure scenario
A
darling.jsoncontaining:makes
--enable-web(or--enable-mcp) execute the attacker's command.ClassifyFirewallPlanreturnsRunElevatedandRunFirewallCommandAsyncexecutes it as administrator.Whoever can write
darling.json— or setDARLING_CONFIG, honored atDarlingConfig.cs:134-137with no path restriction — gets code execution in the elevating operator's context.Fix
Two layers, both cheap:
ReconcileEndpointFirewallAsync, matching every other call site:if (!IPNetwork.TryParse(allowFrom.Trim(), out var cidr)) { /* refuse, do not touch the firewall */ }then passcidr.ToString().BuildFirewallEnableCommand(PowerShell escapes'by doubling) so the builder is safe independent of its callers.Plus a test pinning that a non-CIDR
allowFromnever reaches a firewall command.