fix: flag module __dict__ subscript as reflective attribute access - #517
Open
eitanch228 wants to merge 3 commits into
Open
fix: flag module __dict__ subscript as reflective attribute access#517eitanch228 wants to merge 3 commits into
eitanch228 wants to merge 3 commits into
Conversation
The behavioral AST walker skips every node that is not an ast.Call, so subscripting an imported module's namespace dict - os.__dict__["po"+"pen"] or vars(os)[key] - is invisible even though it is semantically identical to getattr(os, key), which AST7/AST9 already catch. Add an ast.Subscript branch that mirrors the getattr rules: when the base resolves through the import-alias map to a plain module, a non-constant key produces AST7 and a constant key in _DANGEROUS_GETATTR_NAMES produces AST9, both with messages naming the subscript form. Instance attribute bags (self.__dict__[...]) and from-imported classes are deliberately out of scope to avoid false positives. Signed-off-by: eitanch228 <eitan.ch@pluto.security>
fix: flag module __dict__ subscript as reflective attribute access
Empty commit to re-run test-unit after a flaky wall-clock budget failure (is_complete=False from the 60s processing deadline on a slow runner; the failing test scans a markdown-only bundle untouched by this change). Signed-off-by: eitanch228 <eitan.ch@pluto.security>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The behavioral AST analyzer treats
getattras the only reflectiveattribute-access spelling, so the equivalent subscript form is invisible:
getattr(os, computed)-> AST7 (LOW, dynamic attribute access)getattr(os, "popen")-> AST9 (HIGH, literal sink name)os.__dict__["po"+"pen"]-> no finding at all_analyze_pythonwalks the tree and skips every node that is not anast.Call, so anast.Subscriptover a module's__dict__(or overvars(module)) is never inspected. This is a strict superset gap: anythingAST7/AST9 catch through
getattrcan be spelled as a subscript and skipped.Reproduction
A runnable MCP server that resolves its command-execution sink through the
module namespace dict instead of
getattr:The same server spelled with
getattr(os, _cat("po", "pen"))produces AST7.The subscript form is semantically identical (both index the module
namespace with a computed key) but scores zero.
Fix
Mirror the existing getattr rules for the subscript form.
1. New helper
_reflective_module_dict_base(node, aliases). Matches<module>.__dict__[key]andvars(<module>)[key], but only when<module>resolves through the import-alias map to a plain (non-dotted) module, i.e.
import osorimport os as o. This deliberately excludes:self.__dict__[...]and other instance attribute bags, which are a commonand idiomatic pattern (false-positive risk)
from x import SomeClass; SomeClass.__dict__[k]),whose alias resolves to a dotted path
2. A
ast.Subscriptbranch in the walk loop, reusing the existing ruleIDs so severity, confidence, and remediation text stay consistent with the
getattr form:
os.__dict__[computed]/vars(os)[computed]os.__dict__["popen"](constant key in_DANGEROUS_GETATTR_NAMES)os.__dict__["environ"](constant, not a sink name)getattr(obj, "name")After the fix, the reproduction server scores AST7 (LOW, score 3) instead of
zero, the same treatment the getattr spelling already gets. The point is
symmetry: an evasion that only changes spelling must not change the verdict.
Why no new rule IDs
This is the same issue class as AST7/AST9 with a different surface syntax.
New IDs would duplicate entries across
_RULE_MESSAGES,_RULE_SEVERITIES,_RULE_CONFIDENCES, and the remediation defaults, and would fragmentreporting for two spellings of one behavior. The message override makes the
spelling explicit in the finding text.
Out of scope (noted for follow-up)
getattr(os, "__dict__")[key]chainsglobals()["po"+"pen"]/locals()[...]namespace-dict writesdecision that applies to the getattr form equally and should be argued
separately
Test plan
os.__dict__["po"+"pen"]-> AST7os.__dict__["popen"](constant) -> AST9vars(os)[computed]-> AST7import os as o; o.__dict__[k]-> AST7 (alias resolution)self.__dict__[key]-> no finding (instance attribute bag)os.__dict__["environ"]-> no finding (constant, not a sink name)