Repository navigation
v2.2.3
Patch Changes
-
#127
a3fcc35Thanks @nordscope-fi! - Let a config file say "no command-length limit", the way the CLI flag already can (#123).--maxChars=nonedisables the cap and the README documentsnoneor0as doing so. The TOML schema waspositive(), socommandMaxChars = 0was a startup error and the config file had no spelling for it. Moving a flags-based invocation into a config file therefore tightened the limit back to the 5000 default, and the only way to express uncapped was to write9007199254740991out in full.commandMaxCharsand the per-profilemaxCharsare nownonnegative(), with0meaning unlimited. That is the convention this config file already uses forcommandQuotaPerDayandapprovalGrantTtlMs, socommandMaxCharswas the odd one out rather than the key needing a new spelling invented for it.0is mapped rather than merely permitted.sanitizeCommandtestscleaned.length > maxChars, so a literal0arriving there would reject every non-empty command withCommand is too long (max 0 characters)— a worse failure than the one being fixed.normalizeConfigmaps it toNumber.MAX_SAFE_INTEGER, the same valueparseMaxCharsproduces for the flag, so the two surfaces hand the rest of the code an identicalProfilerather than two spellings of uncapped.Negatives stay rejected.
--maxChars=-1means unlimited only as a wart ofparseInthandling, it is undocumented, and it is likelier to be a typo than an intent; parity is worth having between the documented behaviours, not between the accidents. -
#129
369bfcaThanks @tufantunc! - Make command classification linear, so an unbounded command cannot stall the server.Four of the fourteen never-allowed patterns were quadratic:
dd\s.*\bof=/dev/, the twocurl/wgetpipe-into-shell forms, andchown\s+-R\s.*\s/\s*$. Each has a cheap literal head, so the engine matched it at O(n) offsets and dragged a.*or[^|]*across the remainder from each one. A command built by repeating those literals cost 255 ms at 64 KB and 65 seconds at 1 MB, all of it blocking the single-threaded event loop.The stall sits inside
classifyCommand, which runs before the approval gate and before the allow/deny decision — soapprovalPolicy = "ask-all",role = "viewer"andreadOnly = truegave no protection. Onerun-commandcall was enough to stop every other profile, session and in-flight command on the server.The four are now segment checks over the same tokenizer that already decides power-state invocations: split on shell separators, read the head binary past any
sudo, and look at its arguments. Nothing there can backtrack. The same 1 MB command now classifies in 45 ms, and behaviour is unchanged —curl x | sh,dd if=x of=/dev/sda,chown -R root:root /and the rest are refused exactly as before, whilecurl http://x/data.json,dd if=/dev/zero of=/tmp/scratchandchown -R app:app /srv/appstill are not.classifier.tspredicted this in writing: "a policy check should not depend on a limit set three layers away and configurable to any value." Until nowsanitizeCommand'smaxCharswas that limit and the default 5000 kept the cost invisible. Letting a config file saycommandMaxChars = 0removed it, which is what turned a latent property into a reachable one. The check no longer depends on it either way.Also in this change:
[defaults].commandMaxChars = 0is now mapped to the same sentinel as the per-profilemaxChars, so no literal0survives anywhere in the resolved config — a value that would otherwise reject every non-empty command if any caller copied it into a profile. And the README's annotated production profile no longer demonstrates the uncapped spelling on a host it callsprod-web-1; the[defaults]line documents it instead.