fix(security): keep docstrings out of a source body's traversal subjects (#8643) - #8811
Conversation
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound, measured carve-out, but the prose-misread root cause stays in the find pass and the reflection guard's breadth quietly re-arms the false refusal. Watch
Suggestions
[DESIGN-REVIEWED] eec4632 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All evidence is in. Composing the review. First-Principles-Verdict: CONCERNS The fix earns its place, but the new reflection audit respells two existing frozensets, and its dunder-class rule quietly re-refuses ordinary class-based scripts. What this change shipsIntent: stop cron scripts being permanently refused for their prose docstrings (#8643) — a FIX.
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] eec4632 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsI've validated the single candidate and reviewed the actual code. The candidate describes the docstring-exclusion residual: a docstring holding a traversal command that is read back at runtime through a reflection route Working (a)/(b)/(c):
(a) cannot be pinned to a concrete non-exotic input; (b) requires assuming a runtime-assembled route the static walk cannot see. The candidate itself scores "low." It dies under falsification. I also verified the refactor is behavior-preserving: No findings. [OPUS-REVIEWED] eec4632 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
dfbf2b5 to
b9fcf3c
Compare
|
(span=732b722c70fd) |
b9fcf3c to
5c8d211
Compare
|
(span=732b722c70fd) |
5c8d211 to
476f3ab
Compare
|
(span=732b722c70fd) Probe evidence (both treatments agree):
|
476f3ab to
6ba11db
Compare
|
Opus BLOCKING ( |
|
(span=732b722c70fd -- current-head lane pointer only; this disposition's own subject is the round-3 F2 finding below, which the judged head e64218c no longer carries live)
REBUTTAL: real property, wrong attribution; filed as #8830. The truncation lives in the SHARED span finder |
6ba11db to
a16ef28
Compare
bd12cb9 to
7e4c48b
Compare
|
/ai-review override gpt 7e4c48b: The blocking verdict is stale from bd12cb9 whose co_consts finding is fixed and mutation-verified on this head (frame/code group completed), and the provider content-refused this head so no fresh verdict is possible per the workflow's own stale notice.
|
Human judgment recorded@bolichen97 marked the gpt AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
…cts (#8643) A docstring is prose by AST position, and the find-delivery pass reads an English sentence opening with "Find ..." as a find invocation whose word count exhausts its traversal-root budget, refusing fail-closed -- so real cron scripts were permanently refused for their documentation alone (2 of 23 scripts on one real install). Exclude module/class/function docstrings from _source_command_subjects. The exclusion is withdrawn for any body that can read a docstring back (_reads_dunder_doc: __doc__ name/attribute, getdoc, or the "__doc__" constant anywhere), because subprocess.run(f.__doc__, shell=True) executes the docstring verbatim -- such a body keeps the pre-exclusion treatment. The fence scan's docstring retention is unchanged and now shares the position walk via _docstring_constant_ids(types=...). Closes #8643
7e4c48b to
eec4632
Compare
Problem / Motivation
Since #8550,
is_sensitive_source_bodyhands every string constant of a Python source body — docstrings included — to the two traversal passes as per-string command subjects. A prose docstring opening with a verb the traversal grammar models (an English sentence starting "Find …") reads as afindinvocation whose word count exhausts the pass's 64-traversal-root budget, and that budget refuses fail-closed. Real cron scripts are therefore permanently refused for their documentation alone: measured on one real install,pr_security_patrol.pyis refused on current main solely for its"Find commits on main that belong to no pull request…"function docstring, with the misleading verdictBlocked: command traverses a sensitive credential path and delivers the match to a command.Why it matters
Any user cron whose docstrings begin with common English verbs is blocked from scheduling on every fire, permanently, until someone edits the documentation. The refusal text names a credential-path traversal that does not exist, so the user cannot even tell what to fix. The sibling literal-feedback scan measured the same class (#8563/#8564): 3 of 23 real scripts' docstrings drew traversal verdicts while ~3,700 non-docstring literals drew zero.
What changed (motivation → approach → change)
Symptom → root cause: the collection rule in
_source_command_subjectsshipped on the premise that over-collecting "can only ADD denials" because a non-command matches no rule. The premise is false for prose: the find pass parses wide English sentences asfindcommand lines, so over-collecting adds false denials.Change, in three parts:
_docstring_constant_ids(tree, types=(str,))identifies docstring-position constants exactly asast.get_docstringdoes (first statement of a module/class/function body that is a barestrliteral;bytesin that position is not a docstring and stays collected)._source_command_subjectsskips those ids. Non-docstring literals — including bare statement-position strings that are not docstrings, and f-string fragments — flow through unchanged.subprocess.run(f.__doc__, shell=True)executes the docstring verbatim — the complete command sits in the tree as oneConstant, no runtime assembly involved — so the prose argument does not apply there._reads_dunder_docmatches any__doc__name or attribute,getdoc, or the constant"__doc__"anywhere in the tree; such a body keeps every docstring as a subject, the exact pre-exclusion treatment. The guard is name-based and over-broad on purpose: a false positive only restores the stricter behaviour (fail-closed direction). None of the measured real scripts reads__doc__, so the availability fix survives the guard.retainedwalk in_sensitive_run_in_source_literalsnow calls the same helper withtypes=(str, bytes)— its deliberate over-retention (deny direction) now lives in one argument at one call site instead of two hand-maintained walks. The fence scan's behaviour is byte-for-byte unchanged: a docstring naming a fenced store is still denied.Accepted residual (recorded in
docs/system-specs/modules/security.mdalongside the subject-collection description): a verbatim command in docstring position read back through reflection the guard cannot name (a computedgetattrstring) is no longer seen by passes 4/5 — the same runtime-assembly limit the module already records for there-authenticity guards.The
_env_subjectjoin is built from the same collection, so docstrings drop out of it too; a docstring cannot be a fragment of a+-assembled command (it is a standalone statement, not an operand), and the newly-adjacent neighbours in the join can only add a match — both the deny direction.Tests
All in
test/test_security_source_command_subject.py, each mutation-verified (10 mutations, each caught by a distinct test):TestDocstringsAreNotSubjects— the distilled real docstring that Docstrings drawn into #8550's traversal subjects re-block real cron scripts #8643 measured is allowed in all four docstring positions (module/class/function/async); the same text as an assigned literal still denies (the discriminating pair pinning position over content); a bare non-docstring statement string and anif-block first string still deny; a docstring naming a fenced store still denies (fence-scan retention unchanged);bytesin docstring position is still a subject; the fixture-guard asserts the mechanism (traversal-root budget) so the allowed-verdicts cannot go vacuously green.TestDocstringExclusionIsWithdrawnForDocReaders— thef.__doc__/ bare__doc__/getattr(f, "__doc__")/inspect.getdocspellings each keep the docstring a subject and deny; the withdrawal is wholesale (a doc-reading body's prose docstring refuses again, pre-Docstrings drawn into #8550's traversal subjects re-block real cron scripts #8643 treatment); a body without doc reads keeps the exclusion; unit coverage of every_reads_dunder_docspelling.Existing deny-vector and fallback tests (307 in the three touched suites) all pass unchanged.
Manual verification
Ran
mcp_cron._vet_script_contentsover the real cron scripts of one install:pr_security_patrol.py(refused on pristine main solely for its docstring) now vets clean; no other script's verdict changed. Verified the fix fixture is red on pristine main and green on this branch.Related Issues
Closes #8643
Pattern harvest
Rule candidate: review-prompt
Pattern: a scan-subject collection that includes prose positions (docstrings, comments-as-data) feeds command-shaped grammars that model English sentences as invocations — check any new per-string security pass for prose false-positives, and pair any prose exclusion with a reflection-read withdrawal guard.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)