From 9a9f621af9b28eccab27af221a985ebaf2bcde09 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:48:21 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL]?= =?UTF-8?q?=20Fix=20parameter=20default=20expression=20taint=20leakage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tachyon-beep <544926+tachyon-beep@users.noreply.github.com> --- .jules/sentinel.md | 5 +++++ src/wardline/scanner/taint/variable_level.py | 8 ++++---- tests/golden/identity/corpus/META.json | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index a48a87f5..e1f8cdb7 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -7,3 +7,8 @@ **Vulnerability:** The static analyzer was missing `yaml.unsafe_load` and `yaml.full_load` in its `_SERIALISATION_SINKS` mapping, potentially leading to false negatives when tracking untrusted data flowing into these dangerous deserialization functions. **Learning:** Even if functions are listed in rule specifications (like `_SINK_SPECS`), they also need to be properly categorized in the core taint propagation logic (`_SERIALISATION_SINKS`) to ensure the analyzer correctly sheds validation provenance (converting output to `UNKNOWN_RAW`). **Prevention:** When adding new sinks to rule definitions, always verify if they need to be added to core propagation mappings like `_SERIALISATION_SINKS` or `_PROPAGATING_BUILTINS`. + +## 2026-06-28 - Parameter Default Expression Taint Resolution Leak +**Vulnerability:** Untrusted data could leak into `@trusted` functions without triggering `PY-WL-101` because parameter default value expressions were defaulting to the `function_taint` (which is `ASSURED` for trusted functions), masking their true taint in L2 analysis. +**Learning:** Default parameter expressions are evaluated at runtime in the caller's scope when an argument is omitted, not as part of the function body itself. Inheriting the `function_taint` for defaults creates a blind spot where `EXTERNAL_RAW` expressions inside parameter defaults bypass taint checks. +**Prevention:** When evaluating parameter default expressions in static analysis (`_seed_parameters`), always resolve them with a clean base state (`TaintState.INTEGRAL`) rather than inheriting `function_taint` to ensure accurate taint propagation. diff --git a/src/wardline/scanner/taint/variable_level.py b/src/wardline/scanner/taint/variable_level.py index a51a6989..c2576c3b 100644 --- a/src/wardline/scanner/taint/variable_level.py +++ b/src/wardline/scanner/taint/variable_level.py @@ -615,10 +615,10 @@ def _resolve_lambda_body_at_call_inner( default_taints: dict[str, TaintState] = {} defaulted_params = positional_params[len(positional_params) - len(args.defaults) :] for param, default in zip(defaulted_params, args.defaults, strict=True): - default_taints[param.arg] = _resolve_expr(default, function_taint, taint_map, scope) + default_taints[param.arg] = _resolve_expr(default, TaintState.INTEGRAL, taint_map, scope) for param, kw_default in zip(args.kwonlyargs, args.kw_defaults, strict=True): if kw_default is not None: - default_taints[param.arg] = _resolve_expr(kw_default, function_taint, taint_map, scope) + default_taints[param.arg] = _resolve_expr(kw_default, TaintState.INTEGRAL, taint_map, scope) def _omitted_seed(param_name: str) -> TaintState: default = default_taints.get(param_name) @@ -767,12 +767,12 @@ def _seed_parameters( param_idx = total_pos_args - num_defaults + i if 0 <= param_idx < len(all_pos_params): param_name = all_pos_params[param_idx].arg - default_taints[param_name] = _resolve_expr(default_expr, function_taint, taint_map or {}, {}) + default_taints[param_name] = _resolve_expr(default_expr, TaintState.INTEGRAL, taint_map or {}, {}) # Map keyword-only parameter defaults for param, kw_default_expr in zip(args.kwonlyargs, args.kw_defaults, strict=True): if kw_default_expr is not None: - default_taints[param.arg] = _resolve_expr(kw_default_expr, function_taint, taint_map or {}, {}) + default_taints[param.arg] = _resolve_expr(kw_default_expr, TaintState.INTEGRAL, taint_map or {}, {}) def handle_arg(arg: ast.arg) -> None: fallback = default_taints.get(arg.arg, function_taint) diff --git a/tests/golden/identity/corpus/META.json b/tests/golden/identity/corpus/META.json index 5378e6b9..a3c47998 100644 --- a/tests/golden/identity/corpus/META.json +++ b/tests/golden/identity/corpus/META.json @@ -1,5 +1,5 @@ { "corpus_version": 6, "fingerprint_scheme": "wlfp2", - "reason": "call-site full-span discriminator" + "reason": "Use INTEGRAL for default parameter taint resolution" }