Skip to content

fix(checks): handler-security lint missed getattr access and undecorated helpers (BACKLOG #337) - #224

Merged
wshallwshall merged 5 commits into
mainfrom
plan-handler-lint-recall
Aug 7, 2026
Merged

fix(checks): handler-security lint missed getattr access and undecorated helpers (BACKLOG #337)#224
wshallwshall merged 5 commits into
mainfrom
plan-handler-lint-recall

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Closes BACKLOG #337. Two recall gaps in the handler-security lint: attribute access via getattr was invisible to it, and helpers that are not decorated were never walked.

Both are false-negative gaps -- the lint reported clean on code it could not see, which is the failure mode that matters for a security lint. A lint that misses a class is worse than no lint, because it converts "I should check this" into "already checked".

Per-lane notes, owner decisions and merge notes are in .git/mefor-coord/handoff/LANE-HANDOVER-backlog-session-2e228e.md (not published).

Verified: 5 files; merge-tree against origin/main reports 0 conflicts; working tree clean at handoff.

…ed in undecorated helpers (BACKLOG #337)

Two execution-verified recall gaps in `_check_handler_security` (ADR 0144), closed and pinned by tests.

Gap 1 -- ambient-authority / impure-transform saw only a literal Name/Attribute chain, so a
`getattr(os, "system")("id")` indirection (the outer call's func is itself a Call) was never resolved
and never flagged. `_dotted_call_name` now splices a *constant* `getattr(mod, "name")` into the chain
(`getattr(os, "system")` -> `os.system`), so the ambient-authority deny-list sees it; the same shared
resolver flags a `getattr(time, "time")()` wall-clock read for impure-transform. A *dynamic*
`getattr(os, name)`, a `globals()["os"]` subscript, and any other call-shaped receiver stay unresolved
(None), so benign reflection is not flagged. The opt-in Semgrep leg still carries no getattr pattern
(recorded as a residual, not fixed here).

Gap 2 -- phi-to-log was decorated-scope only, so it never opened the undecorated
`_<feed>_transforms.py` helpers the decompose-by-role convention (docs/CONNECTIONS.md, #226) steers
field-level PHI handling into. phi-to-log now scans every function body -- a decorated @router/@handler
OR an undecorated helper -- keyed on the first positional parameter as the message symbol.
impure-transform stays decorated-scope ONLY: widening it too would re-flag the shipped
`_pdf_mdm_transforms.py` ingest-time wall-clock fallback and red the read-only samples/config
calibration gate (the trade ADR 0144 records). Proven both directions against the real samples before
landing.

Severity is unchanged and deliberately conservative -- it holds in BOTH sandbox postures. The lint is
advisory by default (required only under --strict-handler-security) and pre-deployment. An evasion
reaches only host actions the ADR 0087 sandbox does not confine: DEFAULT_FORBIDDEN_MODULES
(pipeline/sandbox.py) blocks socket/ssl/asyncio/multiprocessing, the I/O-bearing messagefoundry.*
subpackages and cryptography, but not os or subprocess. So under mode=off the author already holds
in-process execution, and under mode=subprocess an evasion still reaches neither the DEK nor the audit
chain -- no PHI-exposure path, no runtime-behaviour change ("a filter, not a fix"). To be re-scored
upward when ADR 0147 (OS-level default-deny; Proposed, no code) lands. MessageFoundry is a not-deployed
beta, so a miss "would slip past a deploying site's CI", never "PHI is exposed".

Docs in this commit describe the same change: ADR 0144's Decision-section phi-to-log bullet is
corrected to the widened every-function-body scope (it still asserted decorated-scope, contradicting
the amendment and the code) and its amendment blockquote is added; the master-test-plan row for this
test file is refreshed (line count 987 -> 1,079 and the three scoping regimes). The BACKLOG #337 banner
flip is a separate follow-on commit.

Tests (tests/test_checks_handler_security.py): amb_pos_getattr_os_system, amb_neg_getattr_dynamic_attr,
impure_pos_getattr_time_time, phi_pos_undecorated_helper_logs_msg, plus
test_widened_phi_to_log_does_not_widen_impure_transform and
test_phi_to_log_undecorated_helper_non_message_local_is_clean. Each was falsified by reverting the
fix: the two getattr positives red when the splice is removed; the phi positive reds when the loop
reverts to decorated-scope; the impure-transform guard reds when impure-transform is widened (which
also reds the real samples calibration); the discrimination test reds when phi-to-log ignores the
message symbol; the dynamic-getattr negative reds when the splice over-fires on a non-constant attr.
…lper recall gaps closed)

Banner-only edit of docs/BACKLOG.md item #337: the open `[filed]` banner becomes a closed `[done]`
banner recording the resolution. Isolated from the code+tests+docs commit per the banner-flip rule.

Only #337's banner line changed. The ranked table and the four census distribution lines are
untouched, and the census was NOT recomputed -- the coordinator reconciles it.
@wshallwshall
wshallwshall enabled auto-merge (squash) August 6, 2026 00:40
@wshallwshall
wshallwshall merged commit 8b86715 into main Aug 7, 2026
33 checks passed
@wshallwshall
wshallwshall deleted the plan-handler-lint-recall branch August 7, 2026 01:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant