Fix config rules not matching env-var-prefixed commands - #146
Conversation
|
Thanks for splitting this out and mirroring #137's two-pass so the explicit My concern is that env assignments aren't inert the way a stray flag is, so stripping them before matching is riskier than the global-flags case.
The security model is explicit on this one: variables "stay literal … we match the literal string," expanded by the shell only after approval. Stripping the assignment to match Rather than transparently stripping any |
|
The concern about non-inert prefixes is fair — One data point worth factoring in, though: those examples are already auto-allowed on current from dippy.core.analyzer import analyze
from dippy.core.config import Config
from pathlib import Path
cfg = Config(rules=[])
analyze("LD_PRELOAD=/tmp/evil.so ls", cfg, Path("/tmp")).action # allow (ls)
analyze("GIT_SSH_COMMAND=evil git fetch", cfg, Path("/tmp")).action # allow (git fetch)
analyze("DOCKER_HOST=tcp://other docker ps", cfg, Path("/tmp")).action # allow (docker ps)
Proposal: take your "known-inert subset" suggestion, with a user escape hatch.
This fails closed: a variable nobody thought about isn't in the list, so the command falls back to It also gives a migration path for the built-in-path behavior shown above: once the list exists, the same gate can be applied before If this direction works for you I'll update the PR: gate + directive + tests (parsing/merge in |
Config rules like `allow symfony` did not match commands carrying a leading environment-variable assignment (e.g. `COMPOSE_PROJECT_NAME=app symfony ...`), because the matcher checked the raw tokens where the assignment sits before the base command. The permission prompt shows the command as "symfony", so `allow symfony` looks like it should match but did not. Retry config matching against the env-stripped tokens when the raw pass finds nothing. The retry is asymmetric to preserve the literal-match guarantee for the allow direction: - deny/ask matches always apply (stripping only widens what is blocked); - an allow match applies only when every leading assignment names an inert variable, so a non-inert prefix (LD_PRELOAD=, GIT_SSH_COMMAND=, DOCKER_HOST=, ...) cannot ride in under a plain `allow cmd`. Inert variables are a curated default set (INERT_ENV_VARS: locale, color, project-name, ...) plus the LC_* locale family, extensible per project with a new `allow-env NAME` directive (glob patterns matched case-sensitively; a bare `*` is rejected). The gate covers the config-rule path; the built-in safe-command/version/wrapper paths already strip env prefixes (pre-existing) and tightening them is a follow-up. Refs ldayton#136
c409512 to
687cf0b
Compare
|
Implemented the inert-subset approach in 687cf0b (rebased onto current main, single commit). The allow retry no longer strips arbitrary prefixes:
One honest scoping note: this gate is on the config-rule path only. The |
Summary
Config rules like
allow symfonyfail to match commands with a leading environment-variable assignment such asCOMPOSE_PROJECT_NAME=app symfony console cache:clear. Dippy displays this in the permission prompt as "symfony" (it strips the assignment for description/classification), but the config matcher checks the raw tokens — whereCOMPOSE_PROJECT_NAME=appsits before the base command and breaks the prefix match. So the user sees "symfony", writesallow symfony, and Dippy still asks.This is the environment-variable variant of #136 (the case @Djaler raised in a comment:
DOCKER_IMAGE=foo docker compose upnot matchingallow docker compose up).Approach: inert-gated two-pass config matching
The first review (thanks @ldayton) flagged that blindly stripping
FOO=barfor the allow retry breaks the literal-match guarantee —LD_PRELOAD=/tmp/evil.so cmdwould inheritallow cmd. This revision implements the "known-inert subset" suggestion with a user escape hatch.deny FOO=* symfony *.match_commandagainst the env-stripped tokens. The retry is asymmetric:ask.A variable is inert if it is in the curated default
INERT_ENV_VARS(locale, color, project-name:COMPOSE_PROJECT_NAME,NODE_ENV,CI,TZ,LANG,TERM, …), is anLC_*locale setting, or matches a user-definedallow-env NAMEdirective (glob patterns matched case-sensitively; a bare*is rejected). Nothing affecting executable resolution, library loading, code execution, or connection targets (PATH,LD_*,BASH_ENV,PYTHON*,GIT_SSH_COMMAND,DOCKER_HOST,PAGER,EDITOR, …) is in the default set.This fails closed: a variable nobody enumerated isn't inert, so the command falls back to
ask— unlike a denylist of dangerous vars, which fails open on anything not listed.allow-envmirrors the existingpython-allow-moduledirective shape.Scope / known limitation
This gate covers the config-rule path. The built-in safe-command (
SIMPLE_SAFE), version/help, and transparent-wrapper paths already strip leading env assignments before matching (pre-existing behavior onmain— e.g.LD_PRELOAD=… lsis already auto-allowed today), so a non-inert prefix on those still rides in. Tightening them with the sameINERT_ENV_VARSgate is a natural follow-up;INERT_ENV_VARSlives inallowlists.pyprecisely so it can be reused there. Kept out of this PR to bound the scope to the reported bug.Tests
TestConfigEnvVarPrefixMatching(analyzer) covers: inert allow matches; multiple inert prefixes;LC_*prefix; non-inert/unknown/dangerous prefixes blocked toask; mixed inert+non-inert poisons the allow;allow-envextension and its case-sensitivity; deny retry unconditional; explicit raw-token rule wins in step 1.TestParseConfigEnvVars/TestMergeConfigsEnvVarscover directive parsing, value/*rejection, and scope merging. VS Code grammar, snippet, and sample updated. Full suite passes (11,054 tests).