You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Resolve bounded, statically-computable module and attribute strings used by importlib.import_module(...) and getattr(...).
Map reflective handles such as opener = getattr(module, "url" + "open") back to existing network sinks at the exact call site.
Feed those resolved calls into the existing taint engine, so credential or file-read data reaching urllib.request.urlopen produces the existing TT3/TT4 findings.
Respect lexical function scope, source order, reassignment, argument shadowing, and class namespace boundaries.
This intentionally does not classify arbitrary runtime-only attribute names, ordinary public payloads, all dynamic URLs, or socket/chunk-stream behavior. It addresses the statically resolvable reflective urllib portion of #586.
Validation
pytest -q -m "not integration and not provider" tests/ --tb=short: 5,983 passed, 14 skipped, 40 deselected, 4 xfailed
The excluded live CLI integration file invokes locally installed external agents; the local claude command exited with code 1 and no stderr, which is unrelated to this change.
AI assistance was used to investigate, implement, and test this change; the resulting behavior and diff were manually reviewed and validated.
The resolver treats every unqualified call spelled getattr as the builtin, but it never checks the active scope for a parameter or local binding with that name. For example, def send(getattr): ... opener = getattr(module, "urlopen") ... opener(secret) is recorded as a urllib sink even though the call is user-defined; require the builtin/unshadowed getattr before creating the alias.
Alias invalidation misses non-assignment rebinding constructs
Alias invalidation is implemented only for Assign and AnnAssign. Rebinding through constructs such as for opener in ..., with ... as opener, an assignment expression, an exception target, or del opener is not processed, so the previous reflective sink identity can survive to a later opener(secret) after the runtime binding has changed or been removed. Handle these binding/deletion nodes (or conservatively clear the alias) in source order.
Lambda parameters do not shadow reflective aliases
Only named function definitions establish a new resolver scope; lambdas are traversed generically. A lambda parameter can therefore fail to shadow an outer reflective handle, e.g. lambda opener: opener(secret) is resolved against the module's urlopen alias and can produce a false TT3. Add lambda argument/body scope handling consistent with the named-function path.
Nested methods are omitted from reflective sink analysis
This handler never traverses node.body, so every class method is omitted from call_sinks, including a method that creates and invokes its own reflective urlopen handle. A valid class Client: def send(...): module = ...; opener = getattr(...); opener(secret) therefore remains unreported; the added regression only proves that a class-body handle does not leak into a method. Traverse nested function/async-function definitions with fresh function scopes while still skipping class assignments.
The reason will be displayed to describe this comment to others. Learn more.
Requesting changes for six additional issues reproduced on 1d8a648. The latest commit fixes importlib parameter shadowing and function-local imports. I also reproduced the class-method, rebinding/lambda, and shadowed-getattr issues already described in the existing review; those are referenced rather than duplicated inline.
Compared d162d9b to 1d8a648 using fresh wheels and isolated offline source/wheel runs. The selected regression tests pass (87 base, 98 head), while 15 of 33 focused behavior cases fail identically in both head modes. All eight concurrency checks and seven string-boundary checks pass. Nine CLI fixtures and 12 complete skill directories ran in all four lanes, with source/wheel parity. Nine corpus reports per lane remain partial because of missing references or obfuscated text; this does not establish global rule accuracy or live-provider coverage.
This review covers the reflective urllib scope of this PR, not the remaining dynamic-URL/socket/chunk-stream requirements of #586.
The reason will be displayed to describe this comment to others. Learn more.
Please keep this traversal safe for deep, valid Python expressions. A roughly 1.2 KB file containing a 600-term 1+1+... assignment followed by ordinary urllib.request.urlopen(os.environ.get("API_KEY")) hits RecursionError in this recursive visitor. The baseline detects TT3; this revision loses that finding and the full CLI reports the taint analyzer as failed. An iterative or explicitly bounded traversal needs to preserve normal direct-sink analysis too.
The reason will be displayed to describe this comment to others. Learn more.
Addressed in a797b06. Expression traversal is now iterative, including the local-binding collector used for function bodies, so deeply nested valid expressions no longer abort reflective resolution. Added regressions with a 600-term expression at module and function scope; the following ordinary urllib sink still produces TT3.
The reason will be displayed to describe this comment to others. Learn more.
Please preserve node.level when classifying imports. from .importlib import import_module as load imports a local package module, but this code records it as the standard-library importlib.import_module. Consequently module = load("urllib.request"); opener = getattr(module, "urlopen"); opener(os.environ.get("API_KEY")) is reported as TT3 even though the local loader has no established relationship to urllib. Relative imports should not acquire a known standard-library identity from their spelling.
The reason will be displayed to describe this comment to others. Learn more.
Addressed in a797b06. ImportFrom bindings are only assigned a standard-library identity when level == 0. Relative imports still shadow the local name, but cannot resolve as importlib.import_module. Added the reported relative-import false-positive regression.
The reason will be displayed to describe this comment to others. Learn more.
An annotation without a value does not reassign the variable. Here, opener = getattr(module, "urlopen"); opener: object; opener(os.environ.get("API_KEY")) loses TT3 because _bind clears the existing handle. Conversely, annotating a harmless lambda with opener: getattr(module, "urlopen") creates a false TT3. Please retain the existing value binding when node.value is None rather than using the annotation as an assigned expression.
The reason will be displayed to describe this comment to others. Learn more.
Addressed in a797b06. AnnAssign now visits the annotation for nested expressions but leaves the existing binding unchanged when value is None; annotations are never treated as assigned values. Added positive coverage for retaining an existing handle and negative coverage for an annotation-shaped getattr expression.
The reason will be displayed to describe this comment to others. Learn more.
Resolving a function body immediately freezes its free globals at definition time. If send() calls opener(secret), defining send before the module-level opener = getattr(module, "urlopen") and then calling send() misses TT3. Reversing the order and replacing opener with a harmless lambda before send() instead produces a false TT3. Please account for the bindings visible when the function can run, rather than treating def as execution of its body.
The reason will be displayed to describe this comment to others. Learn more.
Addressed in a797b06. Named function bodies are deferred until the enclosing block binding state is complete, then analyzed against an isolated snapshot. Added regressions for a global handle bound after the definition and for a previously bound handle replaced before the function can run.
The reason will be displayed to describe this comment to others. Learn more.
Both sides of a conditional currently mutate the same binding state in AST order. With if input(): opener = getattr(module, "urlopen") followed by else: opener = lambda value: value, the later opener(os.environ.get("API_KEY")) produces no TT3 because visiting the else branch erases the possible network sink. Please preserve feasible bindings across control-flow joins; visiting the last branch should not decide which runtime path occurred.
The reason will be displayed to describe this comment to others. Learn more.
Addressed in a797b06. If branches now execute from cloned input states and merge possible module/callable bindings at the join instead of letting AST visitation order choose the result. Added coverage for one feasible sink branch and for both branches replacing a stale handle.
The reason will be displayed to describe this comment to others. Learn more.
The collected Store names are not always locals of this function. In def send(): global opener; opener(os.environ.get("API_KEY")); opener = lambda value: value, the later assignment causes the valid outer urlopen handle to be shadowed before its first use, so TT3 is missed. A comprehension target also incorrectly hides an outer handle used after the comprehension. Please exclude global/nonlocal declarations and names belonging to nested expression scopes when collecting function locals.
The reason will be displayed to describe this comment to others. Learn more.
Addressed in a797b06. The local collector now excludes global/nonlocal declarations and nested comprehension scopes. Binding updates honor global/nonlocal targets, while comprehensions get an isolated target scope. Added regressions for the reported global-before-reassignment and comprehension-target cases.
Pushed a797b06 to address the requested reflective-scope follow-ups.
In addition to the six inline cases, this update covers the four previously referenced findings:
unqualified getattr must be the unshadowed builtin;
reflective handles are invalidated by for/async for, with as, assignment expressions, exception targets, del, and augmented assignment;
lambda arguments establish their own shadowing scope;
class methods are analyzed with fresh function scope without closing over the class namespace.
The resolver now uses iterative expression traversal, deferred function-body analysis, isolated comprehension/lambda scopes, and branch-state joins. I added 19 regression tests for the reported cases and adjacent positive/negative controls.
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
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.
Addresses part of #586.
Summary
importlib.import_module(...)andgetattr(...).opener = getattr(module, "url" + "open")back to existing network sinks at the exact call site.urllib.request.urlopenproduces the existing TT3/TT4 findings.This intentionally does not classify arbitrary runtime-only attribute names, ordinary public payloads, all dynamic URLs, or socket/chunk-stream behavior. It addresses the statically resolvable reflective urllib portion of #586.
Validation
pytest -q -m "not integration and not provider" tests/ --tb=short: 5,983 passed, 14 skipped, 40 deselected, 4 xfailedpytest -q -m integration tests/ --ignore=tests/integration/test_agent_cli_live.py --tb=short: 30 passedruff check src/ tests/ruff format --check src/ tests/mypy src/skillspector/nodes/analyzers/behavioral_taint_tracking.pyThe excluded live CLI integration file invokes locally installed external agents; the local
claudecommand exited with code 1 and no stderr, which is unrelated to this change.AI assistance was used to investigate, implement, and test this change; the resulting behavior and diff were manually reviewed and validated.