Skip to content

safety: close four classifier coverage gaps - #4081

Merged
trungutt merged 6 commits into
docker:mainfrom
trungutt:safety-classifier-coverage-gaps
Aug 31, 2026
Merged

safety: close four classifier coverage gaps#4081
trungutt merged 6 commits into
docker:mainfrom
trungutt:safety-classifier-coverage-gaps

Conversation

@trungutt

@trungutt trungutt commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Why

The shell-safety classifier reports unknown — losing the blast-radius label and category tag it would otherwise expose — for a set of commands that are unambiguously destructive, and under-reports the blast radius on two docker commands where a critical flag is present but appears in a non-canonical position. Permissive safety modes end up asking for confirmation without saying why, and (for the flag-order cases) miss that the user is asking to delete named volumes.

Four independent gaps, one commit each:

1. Flag-order variants for docker system prune --volumes and docker compose down -v

Destructive patterns are matched contiguously. docker system prune --volumes (HIGH) does not cover docker system prune -f --volumes because -f sits between the two anchor tokens, so the command falls back to bare docker system prune (MEDIUM) even though the --volumes flag is present and named volumes will be wiped.

The same shape recurs on compose: docker compose down --volumes (HIGH) does not cover docker compose down --remove-orphans -v or docker compose down -v --remove-orphans.

Fix: enumerate the observed flag orderings for -f / -a / -af / -fa / -v / --volumes / --remove-orphans.

2. PowerShell destructive patterns

The taxonomy is Unix-only. Windows users running the shell tool in PowerShell produce destructive commands (Remove-Item -Path <path> -Recurse -Force, Clear-Content) that classify as unknown. Add fs-delete and fs-overwrite entries for the PowerShell equivalents, covering positional/-Path/trailing-path forms and both -Recurse -Force and -Force -Recurse orderings.

3. Destructive SQL, app-CLI, and key-value patterns

Destructive intent frequently arrives via a wrapper — docker exec <ctr> …, kubectl exec …, an SSH login — that hides the dangerous verb inside the inner command. Because the existing taxonomy only inspects the outer shell verb, real data-loss commands (DROP DATABASE, TRUNCATE TABLE, redis-cli FLUSHALL, n8n db:reset, rails db:reset, bench new-site --force, …) come back as unknown.

Add pattern groups that fire on the destructive verb wherever it appears, so wrapping through docker exec / kubectl exec / ssh inherits the classification automatically:

  • SQL DDL/DML: drop database, drop schema, drop table, truncate table, delete from (covers mysql -e, psql -c, sqlite3 db "…", mariadb -e).
  • App-framework resets: n8n db:reset, n8n user-management:reset, bench new-site --force, bench drop-site, rails db:drop / db:reset / db:schema:load, manage.py flush, prisma migrate reset, sequelize db:drop.
  • Key-value flushes: redis-cli flushall, redis-cli flushdb.

4. Whitespace anchor for shell-metachar patterns

The destructive-pattern regex prefix is (?:^|.*\b), which fires on any word boundary. That's the right anchor for alphanumeric verbs (rm, docker), but wrong for patterns whose leading token is itself a shell metacharacter — >, <, |, &, ;. A word boundary sits between any word char and the metachar, so:

  • --email <EMAIL> --firstName Admin matched the truncate pattern > <file> via the l> boundary inside a redacted placeholder. This is the only case that reproduced on master; the placeholder shape (word-char, >, space) is what defeats the \b anchor.

Anchor shell-metacharacter patterns on \s instead: a real redirect, pipe, or chain operator is preceded by whitespace (or start of line) after normalizeCommand collapses runs to single spaces. Verbal patterns keep the \b anchor.

Notes for the reviewer

Each commit is standalone and lands one gap. Regression tests are colocated in safety_test.go in each commit.

…d compose down -v

Destructive-pattern matching is contiguous: 'docker system prune --volumes'
(HIGH) does not match 'docker system prune -f --volumes' because -f sits
between the two anchor tokens, so the command falls back to bare
'docker system prune' (MEDIUM) and under-reports the blast radius when the
--volumes flag is actually present.

Same story on the compose side: 'docker compose down --volumes' (HIGH)
does not cover 'docker compose down --remove-orphans -v' or
'docker compose down -v --remove-orphans', which land on bare
'docker compose down' (LOW-MEDIUM) even though named volumes are wiped.

Add explicit variants for every observed ordering of -f, -a, -af, -fa, -v,
--volumes, and --remove-orphans, plus regression tests.
The destructive taxonomy is Unix-only; Windows users running the shell tool
in PowerShell can execute destructive commands (Remove-Item, Clear-Content)
that the classifier reports as unknown, so the UI cannot label them and
permissive safety modes lose the chance to warn on blast radius.

Add PowerShell equivalents for the fs-delete and fs-overwrite categories,
covering:

- Remove-Item ... -Recurse -Force / -Force -Recurse (HIGH, rm -rf equivalent)
- Remove-Item ... -Recurse (HIGH, still irreversible on non-tty)
- Remove-Item ... -Force (MEDIUM, suppresses confirmation)
- Remove-Item <path> (LOW, prompts on tty)
- Clear-Content (MEDIUM, in-place truncate)

Positional, -Path, and trailing-path forms are enumerated so the observed
argument orderings all match. Argument matching is already case-insensitive
via normalizeCommand, so 'remove-item' and 'REMOVE-ITEM' also match.
Destructive intent frequently arrives via a wrapper — 'docker exec <ctr> …',
'kubectl exec …', an SSH login — that hides the dangerous verb inside the
inner command. The current taxonomy only inspects the outer shell, so real
data-loss commands come back as unknown and permissive safety modes can't
tell the UI what the blast radius actually is.

Add pattern groups that fire on the destructive verb wherever it appears in
the command line, so wrapping through docker exec / kubectl exec / ssh
inherits the classification automatically:

- SQL DDL/DML (drop database, drop table, drop schema, truncate table,
  delete from) — matches across mysql, mariadb, psql, sqlite3, and the -e
  or -c one-shot forms these clients accept.
- App-framework reset verbs (n8n db:reset and user-management:reset, Frappe
  bench new-site --force / drop-site, Rails db:drop / db:reset /
  db:schema:load, Django manage.py flush, Prisma migrate reset,
  Sequelize db:drop) — one-shot commands that wipe or replace application
  state.
- Key-value flushes (redis-cli flushall / flushdb).

Regression tests cover the docker-exec-wrapped forms as well as the bare
client invocations.
@trungutt
trungutt marked this pull request as ready for review August 31, 2026 13:17
@trungutt
trungutt requested a review from a team as a code owner August 31, 2026 13:17
…oundaries

The destructive-pattern regex prefix is (?:^|.*\b), which fires on any
word boundary in the normalised command. That's the right anchor for
alphanumeric verbs like "rm" or "docker", but wrong for patterns whose
leading token is itself a shell metacharacter — ">", "<", "|", "&", ";".
A word boundary sits between any word char and the metachar, so:

- "--email <EMAIL> --firstName Admin" matches the truncate pattern
  "> <file>" via the l> boundary inside the redacted placeholder.
- 'git commit -m "feat: 1>0 check"' matches the same pattern via 1>0.
- Any placeholder or in-string comparison that happens to sit between two
  word characters spuriously escalates to destructive/fs-overwrite.

Anchor shell-metacharacter patterns on \s instead: a real redirect,
pipe, or chain operator is preceded by whitespace (or the start of the
line) after normalizeCommand collapses runs to single spaces. Verbal
patterns keep the \b anchor so 'cd /tmp && rm -rf foo' still resolves
its embedded 'rm -rf' correctly.
@trungutt
trungutt force-pushed the safety-classifier-coverage-gaps branch from 5dcde06 to 05937b1 Compare August 31, 2026 13:22
@aheritier aheritier added area/security Authentication, authorization, secrets, vulnerabilities kind/security Security fix or hardening PR labels Aug 31, 2026
@trungutt
trungutt requested a review from docker-agent August 31, 2026 13:37

@Sayt-0 Sayt-0 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid direction: each gap is real, the commits are atomic, and the whitespace-anchor fix removes a confirmed false positive. Two behavior changes need attention before merge, and roughly half of the new pattern entries are already covered by existing ones.

Verified behavior deltas (master vs this branch):

command master this PR restricted mode impact
grep "drop table users" -r . safe destructive / high ALLOW to DENY
git log --grep "delete from cart" safe destructive / medium ALLOW to DENY
rg "DELETE FROM users" safe destructive / medium ALLOW to DENY
run 1> /tmp/out.log destructive / medium unknown none (ask either way)
docker exec ... --email <EMAIL> ... destructive / medium (FP) unknown none

Blocking:

  • The unanchored SQL patterns flip safe-listed read-only searches to destructive (table above). Since destructive matching wins over the safe list, any grep/rg/git log --grep whose argument contains SQL text now prompts under balanced and is denied under restricted. This trade-off may be acceptable (precedent: grep "rm foo" already matches rm <single-file>), but it is a silent behavior change for a common agent workflow and deserves an explicit decision, a note in the taxonomy, and regression tests either way.

Non-blocking but worth fixing in the same pass:

  • 6 of the 9 new docker entries are already covered by existing patterns (details inline); the pattern language's ... wildcard can replace the enumeration entirely.
  • Remove-Item "C:\Program Files\..." -Recurse -Force classifies LOW because <path> compiles to \S+ and cannot span a quoted path with spaces (details inline).
  • redis-cli -h host FLUSHALL and bench new-site <site> --force fall back to unknown (details inline).

Notes:

  • Two claims in the PR description do not reproduce on master: git commit -m "feat: 1>0 check" never matched (the truncate pattern requires a space after >), and docker compose down -v --remove-orphans was already HIGH via docker compose down -v (the compiled suffix (?:$|\b.*) accepts trailing content). Worth correcting so the taxonomy history stays accurate.
  • The \s anchor drops fd-number redirects (1> file, 2> file) from the truncate pattern. Gating is unchanged (destructive and unknown gate identically), and the 2> /dev/null false positive disappears, so this looks like an acceptable trade; a sentence in the commit message or pattern notes would make it deliberate.

Comment thread pkg/safety/safety_patterns.json Outdated
Comment thread pkg/safety/safety_patterns.json Outdated
Comment thread pkg/safety/safety_patterns.json Outdated
Comment thread pkg/safety/safety_patterns.json Outdated
Comment thread pkg/safety/safety_patterns.json Outdated
Comment thread pkg/safety/classify.go
Comment thread pkg/safety/safety_test.go
…w SQL patterns

Addresses the review on docker#4081:

- Docker: the classifier's compiled patterns already have a trailing
  '\b.*' suffix, so 'docker system prune --volumes' already covers
  '--volumes -f/-a/-af/-fa' on master; enumerating those orderings was
  redundant. Drop them and add wildcard entries 'docker system prune
  ...--volumes', 'docker compose down ...-v', and 'docker compose down
  ...--volumes' to close the only genuine gaps ('-f --volumes',
  '-fa --volumes', and '--remove-orphans -v/--volumes').

- PowerShell: '<path>' compiles to '\S+' and cannot span a quoted
  path with spaces ('C:\Program Files\…', 'OneDrive - Corp\…'), so
  the enumerated -Recurse/-Force orderings fell through to LOW on
  every Windows path that contains a space. Collapse the 14 Remove-
  Item entries to two wildcard forms plus a positional base case.

- Redis / Frappe: connection flags on 'redis-cli' ('-h', '-p', '-n')
  and the site-first ordering on 'bench new-site … --force' broke
  contiguity and reported unknown. Wildcards fix both.

- SQL: 'drop database' / 'drop table' / 'delete from' matched inside
  the quoted arguments of safe-listed search commands, flipping
  'grep "drop table users" -r .' from safe to destructive/high.
  Under 'restricted' this changes ALLOW to DENY on a common workflow.
  Anchor every SQL DDL/DML pattern on a known client executable
  (mysql / mariadb / psql / sqlite3); the leading '.*\b' anchor keeps
  wrapper commands (docker exec, kubectl exec, ssh) matching. Add
  regression tests both for the safe cases and for the accepted
  precedence caveat (destructive wins when the client name itself
  appears in the search argument).
@trungutt
trungutt requested review from a team and Sayt-0 August 31, 2026 14:06
@trungutt
trungutt merged commit 9c8c5bf into docker:main Aug 31, 2026
18 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/security Authentication, authorization, secrets, vulnerabilities kind/security Security fix or hardening PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants