Skip to content

Security: PowerShell command injection via allowFrom in --enable-web / --enable-mcp firewall verbs #1646

Description

@erikdarlingdata

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:

  1. 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().
  2. 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.

Activity

  1. added a commit that references this issue on Jul 25, 2026
  2. erikdarlingdata commented on Jul 25, 2026

    @erikdarlingdata
    OwnerAuthor

    Fixed in #1655, merged to dev as aa01e788.

    (Merging to dev rather than main means GitHub does not auto-close from the PR body, so closing by hand. Ships in the next release off main.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions