4f0092888eb7
service allowlists start/stop/restart/enable/disable before forwarding a verb to sudo systemctl, rejecting unlisted verbs like mask/kill/revert with subcommand help and a non-zero exit instead of running them unconfirmed and undocumented (NINA-011)
- time
- 2026-07-02 13:46 UTC · 3 months ago
- machine
- koh2
- agent
- claude-sonnet-5
- session
- fable audit fix — commands.zig
- parent
- 721ba656a3a3
↓ nina-4f009288.tar.gzatlas at this save
files changed1 file
src/commands.zig+36 −1src/commands.zig+36 −1
@@ -1726,6 +1726,18 @@17261726 .{ "disable <svc>", "disable at boot" },17271727 };17281728 1729+// Verbs service() is willing to forward to `sudo systemctl`. Anything else1730+// (mask, kill, revert, edit, isolate, ...) used to reach systemctl with no1731+// confirmation and without appearing in service_help, since the dispatcher1732+// forwarded ctx.sub verbatim after list/logs/status (NINA-011).1733+fn serviceVerbAllowed(verb: []const u8) bool {1734+ const known = [_][]const u8{ "start", "stop", "restart", "enable", "disable" };1735+ for (known) |v| {1736+ if (std.mem.eql(u8, verb, v)) return true;1737+ }1738+ return false;1739+}1740+17291741 pub fn service(ctx: Ctx) !void {17301742 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("service", &service_help, "add --user to act on user services");17311743 if (std.mem.eql(u8, ctx.sub, "list")) {@@ -1748,7 +1760,13 @@17481760 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, "status", name, ctx.user);17491761 return;17501762 }1751- // start/stop/restart/enable/disable1763+ // start/stop/restart/enable/disable — checked against the allowlist before1764+ // ctx.args is even read, so an unrecognized verb never gets far enough to1765+ // touch a target service name.1766+ if (!serviceVerbAllowed(ctx.sub)) {1767+ output.subcommandHelp("service", &service_help, "add --user to act on user services");1768+ return error.Internal;1769+ }17521770 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;17531771 if (ctx.cfg.confirm and (std.mem.eql(u8, ctx.sub, "stop") or std.mem.eql(u8, ctx.sub, "disable"))) {17541772 if (!output.confirm("")) return;@@ -1756,6 +1774,23 @@17561774 output.serviceVerb(ctx.sub, name, ctx.machine.name);17571775 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, ctx.sub, name, ctx.user);17581776 output.serviceDone();1777+}1778+1779+test "serviceVerbAllowed allows only the verbs listed in service_help" {1780+ try std.testing.expect(serviceVerbAllowed("start"));1781+ try std.testing.expect(serviceVerbAllowed("stop"));1782+ try std.testing.expect(serviceVerbAllowed("restart"));1783+ try std.testing.expect(serviceVerbAllowed("enable"));1784+ try std.testing.expect(serviceVerbAllowed("disable"));1785+}1786+1787+test "serviceVerbAllowed rejects verbs systemctl accepts but nina never confirmed or documented" {1788+ try std.testing.expect(!serviceVerbAllowed("mask"));1789+ try std.testing.expect(!serviceVerbAllowed("kill"));1790+ try std.testing.expect(!serviceVerbAllowed("revert"));1791+ try std.testing.expect(!serviceVerbAllowed("edit"));1792+ try std.testing.expect(!serviceVerbAllowed("isolate"));1793+ try std.testing.expect(!serviceVerbAllowed("starting")); // no substring match on "start"17591794 }17601795 17611796 // --- Flake commands ---