Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-21 - Evaluate Default Argument Expressions with Clean State
**Vulnerability:** Default parameter expressions in Python functions were being evaluated using the function's overall `function_taint` instead of a clean, baseline state.
**Learning:** For a taint analysis engine, parameter defaults must be evaluated independently of the current call context's `function_taint`. If an untrusted default expression uses `function_taint` (e.g. `ASSURED`), it can inadvertently launder untrusted data by marking it as safe simply because the enclosing function is trusted.
**Prevention:** In `_seed_parameters` within `src/wardline/scanner/taint/variable_level.py`, compute the taint of default arguments using `TaintState.INTEGRAL` rather than passing `function_taint` down as the base.
4 changes: 2 additions & 2 deletions src/wardline/scanner/taint/variable_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}, {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve fail-closed taint for unknown default calls

When a default expression contains an unmodelled bare call, _resolve_call uses the supplied seed as its fallback; by passing INTEGRAL here, def f(x=unknown_provider()): return x now seeds x as clean instead of preserving the engine’s fail-closed unknown/raw fallback for unresolved calls. This can make L2 return summaries and downstream trusted callers treat data from unmodelled default providers as trusted, so the default evaluation needs a clean call-context without turning unresolved expressions into INTEGRAL.

Useful? React with 👍 / 👎.


# 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)
Expand Down