use crate::rules::common::AliasTable;
use crate::rules::cross_file::{CrossFileSummaryMap, FunctionTaintSummary};
use crate::rules::taint_engine::{
analyze_function_generic, attribution_hint_for_sink, build_batched_taint_groups,
cross_file_taint_finding, extract_cross_file_summary_for_function,
extract_cross_file_summary_for_function_cf, match_binop_format_sink, match_call_sink,
match_object_literal_sink, match_return_value_sink, node_text, push_attributed_findings,
summarize_function_return_generic, taint_finding_for_node_ranged, AnalysisContext,
TaintLanguageAdapter, TaintState,
};
pub use crate::rules::taint_engine::{
BatchedRule, NodeMatcher, ReturnSummary, ReturnTaintSummary, RuleFilter, TaintFinding,
TaintSpec,
};
use std::collections::HashMap;
use std::path::PathBuf;
use tree_sitter::Node;
#[derive(Clone)]
pub struct CrossFileInfo<'a> {
pub import_to_path: &'a HashMap<String, PathBuf>,
pub summaries: &'a CrossFileSummaryMap,
pub rule_filter: RuleFilter<'a>,
}
type PyCtx<'a> = AnalysisContext<'a, CrossFileInfo<'a>>;
pub fn analyze_tree(
root: Node<'_>,
source: &str,
spec: &TaintSpec,
aliases: Option<&AliasTable>,
) -> Vec<TaintFinding> {
analyze_tree_with_cross_file(root, source, spec, aliases, None)
}
pub fn analyze_tree_with_cross_file<'a>(
root: Node<'_>,
source: &'a str,
spec: &'a TaintSpec,
aliases: Option<&'a AliasTable>,
cross_file: Option<&'a CrossFileInfo<'a>>,
) -> Vec<TaintFinding> {
let empty_summary = ReturnSummary::new();
let mut summaries = ReturnSummary::new();
let pass1_ctx = AnalysisContext {
source,
spec,
aliases,
summaries: &empty_summary,
cross_file: None,
sink_to_rules: None,
label_policy: None,
};
collect_function_defs(root, &mut |func_node| {
let (name, ret_taint) = summarize_function_return(func_node, &pass1_ctx);
if let Some(name) = name {
summaries.insert(name, ret_taint);
}
});
let ctx = AnalysisContext {
source,
spec,
aliases,
summaries: &summaries,
cross_file,
sink_to_rules: None,
label_policy: None,
};
let mut findings = Vec::new();
collect_function_defs(root, &mut |func_node| {
analyze_function(func_node, &ctx, &mut findings);
});
findings
}
pub struct CrossFileInfoBatched<'a> {
pub import_to_path: &'a HashMap<String, PathBuf>,
pub summaries: &'a CrossFileSummaryMap,
}
pub fn analyze_tree_batched<'a>(
root: Node<'_>,
source: &'a str,
rules: &[BatchedRule<'a>],
aliases: Option<&'a AliasTable>,
cross_file: Option<&'a CrossFileInfoBatched<'a>>,
) -> Vec<(String, TaintFinding)> {
if rules.is_empty() {
return Vec::new();
}
let mut out: Vec<(String, TaintFinding)> = Vec::new();
for group in build_batched_taint_groups(rules) {
let empty_summary = ReturnSummary::new();
let pass1_ctx = AnalysisContext {
source,
spec: &group.spec,
aliases,
summaries: &empty_summary,
cross_file: None,
sink_to_rules: None,
label_policy: None,
};
let mut summaries = ReturnSummary::new();
collect_function_defs(root, &mut |func_node| {
let (name, ret_taint) = summarize_function_return(func_node, &pass1_ctx);
if let Some(name) = name {
summaries.insert(name, ret_taint);
}
});
let cross_file_for_group = cross_file.map(|cf| CrossFileInfo {
import_to_path: cf.import_to_path,
summaries: cf.summaries,
rule_filter: RuleFilter::Any(&group.allowed_rule_ids),
});
let ctx = AnalysisContext {
source,
spec: &group.spec,
aliases,
summaries: &summaries,
cross_file: cross_file_for_group.as_ref(),
sink_to_rules: Some(&group.sink_to_rules),
label_policy: None,
};
let mut group_findings: Vec<TaintFinding> = Vec::new();
collect_function_defs(root, &mut |func_node| {
analyze_function(func_node, &ctx, &mut group_findings);
});
push_attributed_findings(&mut out, group_findings, &group.sink_to_rules);
}
out
}
pub fn extract_cross_file_summaries(
root: Node<'_>,
source: &str,
aliases: Option<&AliasTable>,
rule_specs: &[(&str, TaintSpec)],
) -> Vec<FunctionTaintSummary> {
let mut summaries = Vec::new();
collect_function_defs(root, &mut |func_node| {
let Some(name_node) = func_node.child_by_field_name("name") else {
return;
};
let func_name = node_text(name_node, source).to_string();
let param_names = collect_param_names(func_node, source);
if let Some(summary) =
extract_cross_file_summary_for_function::<PyTaintAdapter, CrossFileInfo<'_>>(
func_node,
&func_name,
¶m_names,
source,
aliases,
rule_specs,
)
{
summaries.push(summary);
}
});
summaries
}
pub fn compose_cross_file_summaries(
root: Node<'_>,
source: &str,
aliases: Option<&AliasTable>,
rule_specs: &[(&str, TaintSpec)],
import_to_path: &HashMap<String, PathBuf>,
summaries: &CrossFileSummaryMap,
allowed_rule_ids: &std::collections::HashSet<String>,
) -> Vec<FunctionTaintSummary> {
let cross_file = CrossFileInfo {
import_to_path,
summaries,
rule_filter: RuleFilter::Any(allowed_rule_ids),
};
let mut out = Vec::new();
collect_function_defs(root, &mut |func_node| {
let Some(name_node) = func_node.child_by_field_name("name") else {
return;
};
let func_name = node_text(name_node, source).to_string();
let param_names = collect_param_names(func_node, source);
if let Some(summary) =
extract_cross_file_summary_for_function_cf::<PyTaintAdapter, CrossFileInfo<'_>>(
func_node,
&func_name,
¶m_names,
source,
aliases,
rule_specs,
Some(&cross_file),
)
{
out.push(summary);
}
});
out
}
fn collect_param_names(func_node: Node<'_>, source: &str) -> Vec<String> {
let Some(params) = func_node.child_by_field_name("parameters") else {
return Vec::new();
};
let mut names = Vec::new();
let mut cursor = params.walk();
for child in params.children(&mut cursor) {
let param_name = match child.kind() {
"identifier" => Some(node_text(child, source)),
"typed_parameter" | "default_parameter" | "typed_default_parameter" => {
let mut inner_cursor = child.walk();
let mut found: Option<&str> = None;
for inner in child.children(&mut inner_cursor) {
if inner.kind() == "identifier" {
found = Some(node_text(inner, source));
break;
}
}
found
}
_ => None,
};
if let Some(name) = param_name {
if name != "self" && name != "cls" {
names.push(name.to_string());
}
}
}
names
}
fn function_summary_key(name: &str, arity: usize) -> String {
format!("{name}/{arity}")
}
fn call_summary_key(name: &str, args: Node<'_>) -> String {
let mut cursor = args.walk();
function_summary_key(name, args.named_children(&mut cursor).count())
}
fn summarize_function_return(
func_node: Node<'_>,
ctx: &PyCtx<'_>,
) -> (Option<String>, ReturnTaintSummary) {
let name = func_node
.child_by_field_name("name")
.map(|n| node_text(n, ctx.source).to_string());
let summary =
summarize_function_return_generic::<PyTaintAdapter, _>(func_node, ctx, collect_param_names);
let name = name
.map(|name| function_summary_key(&name, collect_param_names(func_node, ctx.source).len()));
(name, summary)
}
pub(super) struct PyTaintAdapter;
impl<'a> TaintLanguageAdapter<CrossFileInfo<'a>> for PyTaintAdapter {
fn is_nested_scope(kind: &str) -> bool {
kind == "function_definition"
}
fn dispatch_walk_node(
node: Node<'_>,
ctx: &PyCtx<'_>,
state: &mut TaintState,
findings: &mut Vec<TaintFinding>,
) {
if node.kind() == "assignment" {
handle_assignment(node, ctx, state);
}
if node.kind() == "named_expression" {
if let (Some(name), Some(value)) = (
node.child_by_field_name("name"),
node.child_by_field_name("value"),
) {
if name.kind() == "identifier" {
let lhs = node_text(name, ctx.source).to_string();
if let Some(o) = expression_taint(value, ctx, state) {
state.taint_ranged(lhs, o.description, o.line, o.source_range);
} else {
state.clear(&lhs);
}
}
}
}
if node.kind() == "call" {
handle_call(node, ctx, state, findings);
}
if node.kind() == "with_statement" {
handle_with_statement(node, ctx, state);
}
if node.kind() == "binary_operator" || node.kind() == "string" {
handle_binop_format_sink(node, ctx, state, findings);
}
if node.kind() == "dictionary" {
handle_dict_literal_sink(node, ctx, state, findings);
}
if node.kind() == "return_statement" {
handle_return_value_sink(node, ctx, state, findings);
}
}
fn dispatch_summary_node(
node: Node<'_>,
ctx: &PyCtx<'_>,
state: &mut TaintState,
findings: &mut Vec<TaintFinding>,
return_taint: &mut Option<String>,
) {
Self::dispatch_walk_node(node, ctx, state, findings);
if node.kind() == "return_statement" && return_taint.is_none() {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if let Some(o) = expression_taint(child, ctx, state) {
*return_taint = Some(o.description);
break;
}
}
}
}
fn expression_taint(
expr: Node<'_>,
ctx: &PyCtx<'_>,
state: &TaintState,
) -> Option<(String, usize)> {
expression_taint(expr, ctx, state).map(|o| (o.description, o.line))
}
fn seed_params(func_node: Node<'_>, ctx: &PyCtx<'_>, state: &mut TaintState) {
if let Some(params) = func_node.child_by_field_name("parameters") {
let decorators = decorator_method_names(func_node, ctx.source);
seed_param_sources(params, ctx.source, ctx.spec, state, &decorators);
}
}
}
fn decorator_method_names(func_node: Node<'_>, source: &str) -> Vec<String> {
let Some(parent) = func_node.parent() else {
return Vec::new();
};
if parent.kind() != "decorated_definition" {
return Vec::new();
}
let mut names = Vec::new();
let mut cursor = parent.walk();
for child in parent.children(&mut cursor) {
if child.kind() != "decorator" {
continue;
}
let mut dcur = child.walk();
let expr = child.named_children(&mut dcur).next();
let Some(expr) = expr else { continue };
if expr.kind() != "call" {
continue;
}
let Some(callee) = expr.child_by_field_name("function") else {
continue;
};
if let Some(name) = callee_final_name(callee, source) {
names.push(name);
}
}
names
}
fn callee_final_name(callee: Node<'_>, source: &str) -> Option<String> {
match callee.kind() {
"identifier" => Some(node_text(callee, source).to_string()),
"attribute" => callee
.child_by_field_name("attribute")
.map(|a| node_text(a, source).to_string()),
_ => None,
}
}
fn collect_function_defs<'tree, F>(node: Node<'tree>, visit: &mut F)
where
F: FnMut(Node<'tree>),
{
if node.kind() == "function_definition" {
visit(node);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_function_defs(child, visit);
}
}
fn analyze_function(func_node: Node<'_>, ctx: &PyCtx<'_>, findings: &mut Vec<TaintFinding>) {
analyze_function_generic::<PyTaintAdapter, _>(func_node, ctx, findings);
}
fn seed_param_sources(
params: Node<'_>,
source: &str,
spec: &TaintSpec,
state: &mut TaintState,
decorators: &[String],
) {
let mut cursor = params.walk();
for child in params.children(&mut cursor) {
let param_name = match child.kind() {
"identifier" => node_text(child, source),
"typed_parameter" | "default_parameter" | "typed_default_parameter" => {
let mut inner_cursor = child.walk();
let mut found: Option<&str> = None;
for inner in child.children(&mut inner_cursor) {
if inner.kind() == "identifier" {
found = Some(node_text(inner, source));
break;
}
}
match found {
Some(n) => n,
None => continue,
}
}
_ => continue,
};
for matcher in &spec.sources {
match matcher {
NodeMatcher::ParamName { names, description } => {
if names.iter().any(|n| n == param_name)
|| crate::rules::taint_engine::param_names_are_wildcard(names)
{
let line = child.start_position().row + 1;
state.taint(param_name.to_string(), description.clone(), line);
break;
}
}
NodeMatcher::DecoratedParamSource {
decorator,
description,
} if decorators.iter().any(|d| d == decorator) => {
let line = child.start_position().row + 1;
state.taint(param_name.to_string(), description.clone(), line);
break;
}
_ => {}
}
}
}
}
fn handle_assignment(node: Node<'_>, ctx: &PyCtx<'_>, state: &mut TaintState) {
let (Some(left), Some(right)) = (
node.child_by_field_name("left"),
node.child_by_field_name("right"),
) else {
return;
};
if left.kind() == "identifier" {
let lhs_name = node_text(left, ctx.source).to_string();
if let Some(o) = expression_taint(right, ctx, state) {
state.taint_ranged(lhs_name, o.description, o.line, o.source_range);
} else {
state.clear(&lhs_name);
}
return;
}
if is_destructuring_pattern(left) {
let lhs_targets = collect_destructuring_targets(left, ctx.source);
if lhs_targets.is_empty() {
return;
}
if let Some(rhs_elems) = tuple_like_elements(right) {
if rhs_elems.len() == lhs_targets.len() {
for (target, rhs) in lhs_targets.iter().zip(rhs_elems.iter()) {
if let Some(o) = expression_taint(*rhs, ctx, state) {
state.taint_ranged(target.clone(), o.description, o.line, o.source_range);
} else {
state.clear(target);
}
}
return;
}
}
if let Some(o) = expression_taint(right, ctx, state) {
for target in &lhs_targets {
state.taint_ranged(
target.clone(),
o.description.clone(),
o.line,
o.source_range,
);
}
} else {
for target in &lhs_targets {
state.clear(target);
}
}
}
}
fn handle_with_statement(node: Node<'_>, ctx: &PyCtx<'_>, state: &mut TaintState) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() != "with_clause" {
continue;
}
let mut clause_cursor = child.walk();
for item in child.children(&mut clause_cursor) {
if item.kind() != "with_item" {
continue;
}
let mut item_cursor = item.walk();
for item_child in item.named_children(&mut item_cursor) {
if item_child.kind() == "as_pattern" {
handle_as_pattern(item_child, ctx, state);
}
}
}
}
}
fn handle_as_pattern(node: Node<'_>, ctx: &PyCtx<'_>, state: &mut TaintState) {
let mut cursor = node.walk();
let named: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
let value = match named.first() {
Some(n) => *n,
None => return,
};
let alias_ident = named.iter().find_map(|n| {
if n.kind() == "as_pattern_target" {
let mut inner = n.walk();
let found = n
.named_children(&mut inner)
.find(|c| c.kind() == "identifier");
found
} else {
None
}
});
let Some(alias_node) = alias_ident else {
return;
};
let alias_name = node_text(alias_node, ctx.source).to_string();
if let Some(o) = expression_taint(value, ctx, state) {
state.taint_ranged(alias_name, o.description, o.line, o.source_range);
} else {
state.clear(&alias_name);
}
}
fn is_destructuring_pattern(node: Node<'_>) -> bool {
matches!(
node.kind(),
"pattern_list" | "tuple_pattern" | "list_pattern"
)
}
fn collect_destructuring_targets(node: Node<'_>, source: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
match child.kind() {
"identifier" => out.push(node_text(child, source).to_string()),
"pattern_list" | "tuple_pattern" | "list_pattern" => {
out.extend(collect_destructuring_targets(child, source));
}
"list_splat_pattern" => {
let mut inner = child.walk();
for c in child.named_children(&mut inner) {
if c.kind() == "identifier" {
out.push(node_text(c, source).to_string());
}
}
}
_ => {}
}
}
out
}
fn tuple_like_elements<'tree>(node: Node<'tree>) -> Option<Vec<Node<'tree>>> {
match node.kind() {
"expression_list" | "tuple" | "list" => {
let mut cursor = node.walk();
let elems: Vec<Node<'tree>> = node.named_children(&mut cursor).collect();
Some(elems)
}
_ => None,
}
}
fn handle_call(
node: Node<'_>,
ctx: &PyCtx<'_>,
state: &mut TaintState,
findings: &mut Vec<TaintFinding>,
) {
let Some(func) = node.child_by_field_name("function") else {
return;
};
let callee_text = node_text(func, ctx.source);
let resolved = match ctx.aliases {
Some(a) => a.resolve(callee_text).into_owned(),
None => callee_text.to_string(),
};
if let Some(sink) = match_call_sink(ctx.spec, resolved.as_str(), ctx.sink_to_rules) {
let Some(args) = node.child_by_field_name("arguments") else {
return;
};
let mut cursor = args.walk();
for arg in args.named_children(&mut cursor) {
if let Some(o) = expression_taint(arg, ctx, state) {
let rule_hint = attribution_hint_for_sink(&sink);
findings.push(taint_finding_for_node_ranged(
node,
o.description,
sink.description,
o.line,
rule_hint,
1,
o.source_range,
));
break;
}
}
return;
}
if let Some(cross_file) = ctx.cross_file {
handle_cross_file_call(node, func, callee_text, ctx, state, findings, cross_file);
}
}
fn handle_binop_format_sink(
node: Node<'_>,
ctx: &PyCtx<'_>,
state: &mut TaintState,
findings: &mut Vec<TaintFinding>,
) {
let Some(sink) = match_binop_format_sink(ctx.spec, ctx.sink_to_rules) else {
return;
};
if node.kind() == "binary_operator" {
if let Some(parent) = node.parent() {
if parent.kind() == "binary_operator" && binop_is_concat(parent, ctx.source) {
return;
}
}
if !binop_is_concat(node, ctx.source) {
return;
}
}
if node.kind() == "string" && !python_string_is_fstring(node) {
return;
}
let has_string_literal = match node.kind() {
"string" => true, "binary_operator" => binop_has_string_literal_operand(node, ctx.source),
_ => false,
};
if !has_string_literal {
return;
}
if let Some(o) = expression_taint(node, ctx, state) {
let rule_hint = attribution_hint_for_sink(&sink);
findings.push(taint_finding_for_node_ranged(
node,
o.description,
sink.description,
o.line,
rule_hint,
1,
o.source_range,
));
}
}
fn handle_dict_literal_sink(
node: Node<'_>,
ctx: &PyCtx<'_>,
state: &mut TaintState,
findings: &mut Vec<TaintFinding>,
) {
let Some(sink) = match_object_literal_sink(ctx.spec, ctx.sink_to_rules) else {
return;
};
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if child.kind() != "pair" {
continue;
}
let Some(value) = child.child_by_field_name("value") else {
continue;
};
if let Some(o) = expression_taint(value, ctx, state) {
let rule_hint = attribution_hint_for_sink(&sink);
findings.push(taint_finding_for_node_ranged(
node,
o.description,
sink.description,
o.line,
rule_hint,
1,
o.source_range,
));
return;
}
}
}
fn handle_return_value_sink(
node: Node<'_>,
ctx: &PyCtx<'_>,
state: &mut TaintState,
findings: &mut Vec<TaintFinding>,
) {
let Some(sink) = match_return_value_sink(ctx.spec, ctx.sink_to_rules) else {
return;
};
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if let Some(o) = expression_taint(child, ctx, state) {
let rule_hint = attribution_hint_for_sink(&sink);
findings.push(taint_finding_for_node_ranged(
node,
o.description,
sink.description,
o.line,
rule_hint,
1,
o.source_range,
));
return;
}
}
}
fn binop_is_concat(node: Node<'_>, source: &str) -> bool {
node.child_by_field_name("operator")
.map(|op| {
let t = node_text(op, source);
t == "+" || t == "%"
})
.unwrap_or(false)
}
fn node_is_python_string_construction(node: Node<'_>, source: &str) -> bool {
match node.kind() {
"string" => python_string_is_fstring(node),
"binary_operator" => {
binop_is_concat(node, source) && binop_has_string_literal_operand(node, source)
}
"call" => call_is_str_format(node, source),
_ => false,
}
}
fn call_is_str_format(node: Node<'_>, source: &str) -> bool {
let Some(func) = node.child_by_field_name("function") else {
return false;
};
if func.kind() != "attribute" {
return false;
}
let Some(attr) = func.child_by_field_name("attribute") else {
return false;
};
if node_text(attr, source) != "format" {
return false;
}
let Some(obj) = func.child_by_field_name("object") else {
return false;
};
matches!(obj.kind(), "string" | "concatenated_string")
}
fn python_string_is_fstring(node: Node<'_>) -> bool {
let mut cursor = node.walk();
let mut has_interp = false;
for c in node.children(&mut cursor) {
if c.kind() == "interpolation" {
has_interp = true;
break;
}
}
has_interp
}
fn binop_has_string_literal_operand(node: Node<'_>, source: &str) -> bool {
fn operand_has_string(n: Node<'_>, source: &str) -> bool {
match n.kind() {
"string" | "concatenated_string" => true,
"binary_operator" => {
if !binop_is_concat(n, source) {
return false;
}
let left = n.child_by_field_name("left");
let right = n.child_by_field_name("right");
left.map(|l| operand_has_string(l, source)).unwrap_or(false)
|| right
.map(|r| operand_has_string(r, source))
.unwrap_or(false)
}
"parenthesized_expression" => n
.named_child(0)
.map(|c| operand_has_string(c, source))
.unwrap_or(false),
_ => false,
}
}
operand_has_string(node, source)
}
fn handle_cross_file_call(
node: Node<'_>,
func: Node<'_>,
callee_text: &str,
ctx: &PyCtx<'_>,
state: &TaintState,
findings: &mut Vec<TaintFinding>,
cross_file: &CrossFileInfo<'_>,
) {
let resolved = resolve_cross_file_callee(func, callee_text, ctx.source, cross_file);
let Some((file_path, func_name)) = resolved else {
return;
};
let Some(file_summaries) = cross_file.summaries.get(&file_path) else {
return;
};
let Some(summary) = file_summaries.iter().find(|s| s.name == func_name) else {
return;
};
let Some(args) = node.child_by_field_name("arguments") else {
return;
};
let mut cursor = args.walk();
let arg_nodes: Vec<Node<'_>> = args.named_children(&mut cursor).collect();
for flow in &summary.params_to_sink {
if !cross_file.rule_filter.allows(&flow.sink_rule_id) {
continue;
}
if flow.param_index >= arg_nodes.len() {
continue;
}
let arg = arg_nodes[flow.param_index];
if let Some(o) = expression_taint(arg, ctx, state) {
findings.push(cross_file_taint_finding(
node,
o.description,
o.line,
&flow.sink_description,
&func_name,
&flow.sink_rule_id,
));
return;
}
}
}
fn resolve_cross_file_callee(
func: Node<'_>,
callee_text: &str,
source: &str,
cross_file: &CrossFileInfo<'_>,
) -> Option<(PathBuf, String)> {
if func.kind() == "attribute" {
if let Some(object) = func.child_by_field_name("object") {
if object.kind() == "identifier" {
let module_name = node_text(object, source);
if let Some(file_path) = cross_file.import_to_path.get(module_name) {
if let Some(attr) = func.child_by_field_name("attribute") {
let func_name = node_text(attr, source).to_string();
return Some((file_path.clone(), func_name));
}
}
}
}
}
if func.kind() == "identifier" {
for (key, file_path) in cross_file.import_to_path.iter() {
if let Some(rest) = key.strip_prefix("__from__:") {
if let Some((_module, name)) = rest.split_once(':') {
if name == callee_text {
return Some((file_path.clone(), name.to_string()));
}
}
}
}
}
None
}
struct TaintOrigin {
description: String,
line: usize,
source_range: Option<(usize, usize)>,
}
fn expression_taint(expr: Node<'_>, ctx: &PyCtx<'_>, state: &TaintState) -> Option<TaintOrigin> {
let expr_line = expr.start_position().row + 1;
if let Some(desc) = match_source(expr, ctx.source, ctx.spec, ctx.aliases) {
return Some(TaintOrigin {
description: desc,
line: expr_line,
source_range: Some((expr.start_byte(), expr.end_byte())),
});
}
if expr.kind() == "identifier" {
let name = node_text(expr, ctx.source);
if let Some(info) = state.info(name) {
return Some(TaintOrigin {
description: info.description.clone(),
line: info.line,
source_range: info.source_range,
});
}
}
if expr.kind() == "attribute" {
if let Some(object) = expr.child_by_field_name("object") {
if object.kind() == "identifier" {
let name = node_text(object, ctx.source);
if let Some(info) = state.info(name) {
return Some(TaintOrigin {
description: info.description.clone(),
line: info.line,
source_range: info.source_range,
});
}
}
}
}
if expr.kind() == "subscript" {
if let Some(value) = expr.child_by_field_name("value") {
if let Some(result) = expression_taint(value, ctx, state) {
return Some(result);
}
}
}
if expr.kind() == "tuple" || expr.kind() == "list" {
let mut cursor = expr.walk();
for child in expr.named_children(&mut cursor) {
if let Some(result) = expression_taint(child, ctx, state) {
return Some(result);
}
}
}
if matches!(
expr.kind(),
"list_comprehension"
| "set_comprehension"
| "dictionary_comprehension"
| "generator_expression"
) {
let mut cursor = expr.walk();
for child in expr.named_children(&mut cursor) {
if child.kind() == "for_in_clause" {
if let Some(iterable) = child.child_by_field_name("right") {
if let Some(result) = expression_taint(iterable, ctx, state) {
return Some(result);
}
}
}
}
}
if expr.kind() == "string" {
let mut cursor = expr.walk();
for child in expr.children(&mut cursor) {
if child.kind() == "interpolation" {
let mut inner = child.walk();
for inner_child in child.named_children(&mut inner) {
if let Some(result) = expression_taint(inner_child, ctx, state) {
return Some(result);
}
}
}
}
}
if expr.kind() == "conditional_expression" {
if let Some(body) = expr.named_child(0) {
if let Some(result) = expression_taint(body, ctx, state) {
return Some(result);
}
}
if let Some(alternative) = expr.named_child(2) {
if let Some(result) = expression_taint(alternative, ctx, state) {
return Some(result);
}
}
}
if expr.kind() == "binary_operator" {
if let Some(op) = expr.child_by_field_name("operator") {
let op_text = node_text(op, ctx.source);
if op_text == "+" || op_text == "%" {
if let Some(left) = expr.child_by_field_name("left") {
if let Some(result) = expression_taint(left, ctx, state) {
return Some(result);
}
}
if let Some(right) = expr.child_by_field_name("right") {
if let Some(result) = expression_taint(right, ctx, state) {
return Some(result);
}
}
}
}
}
if expr.kind() == "call" {
if is_sanitizer_call(expr, ctx.source, ctx.spec, ctx.aliases) {
return None;
}
if let Some(args) = expr.child_by_field_name("arguments") {
if let Some(func) = expr.child_by_field_name("function") {
if func.kind() == "identifier" {
let callee = node_text(func, ctx.source);
if let Some(summary) = ctx.summaries.get(&call_summary_key(callee, args)) {
if let Some(desc) = &summary.direct_source {
return Some(TaintOrigin {
description: format!("{desc} (via {callee})"),
line: expr_line,
source_range: Some((expr.start_byte(), expr.end_byte())),
});
}
let mut cursor = args.walk();
let arg_nodes: Vec<Node<'_>> = args.named_children(&mut cursor).collect();
for ¶m_idx in &summary.params_to_return {
if param_idx < arg_nodes.len() {
if let Some(o) = expression_taint(arg_nodes[param_idx], ctx, state)
{
return Some(TaintOrigin {
description: format!("{} (via {callee})", o.description),
line: o.line,
source_range: o.source_range,
});
}
}
}
return None;
}
}
}
if args.kind() == "generator_expression" {
if let Some(result) = expression_taint(args, ctx, state) {
return Some(result);
}
}
let mut cursor = args.walk();
for arg in args.named_children(&mut cursor) {
if let Some(result) = expression_taint(arg, ctx, state) {
return Some(result);
}
}
}
if let Some(func) = expr.child_by_field_name("function") {
if func.kind() == "attribute" {
if let Some(attr) = func.child_by_field_name("attribute") {
if node_text(attr, ctx.source) == "format" {
if let Some(args) = expr.child_by_field_name("arguments") {
let mut cursor = args.walk();
for arg in args.named_children(&mut cursor) {
if let Some(result) = expression_taint(arg, ctx, state) {
return Some(result);
}
}
}
}
}
}
}
if let Some(func) = expr.child_by_field_name("function") {
if func.kind() == "attribute" {
if let Some(object) = func.child_by_field_name("object") {
if let Some(result) = expression_taint(object, ctx, state) {
return Some(result);
}
}
}
}
if let Some(func) = expr.child_by_field_name("function") {
if func.kind() == "identifier" {
let callee = node_text(func, ctx.source);
if let Some(args) = expr.child_by_field_name("arguments") {
if let Some(summary) = ctx.summaries.get(&call_summary_key(callee, args)) {
if let Some(desc) = &summary.direct_source {
return Some(TaintOrigin {
description: format!("{desc} (via {callee})"),
line: expr_line,
source_range: Some((expr.start_byte(), expr.end_byte())),
});
}
let mut cursor = args.walk();
let arg_nodes: Vec<Node<'_>> = args.named_children(&mut cursor).collect();
for ¶m_idx in &summary.params_to_return {
if param_idx < arg_nodes.len() {
if let Some(o) = expression_taint(arg_nodes[param_idx], ctx, state)
{
return Some(TaintOrigin {
description: format!("{} (via {callee})", o.description),
line: o.line,
source_range: o.source_range,
});
}
}
}
}
}
}
}
if let Some(cross_file) = ctx.cross_file {
if let Some(func) = expr.child_by_field_name("function") {
let callee_text = node_text(func, ctx.source);
if let Some((file_path, func_name)) =
resolve_cross_file_callee(func, callee_text, ctx.source, cross_file)
{
if let Some(file_summaries) = cross_file.summaries.get(&file_path) {
if let Some(summary) = file_summaries.iter().find(|s| s.name == func_name) {
if let Some(args) = expr.child_by_field_name("arguments") {
let mut cursor = args.walk();
let arg_nodes: Vec<Node<'_>> =
args.named_children(&mut cursor).collect();
for ¶m_idx in &summary.params_to_return {
if param_idx < arg_nodes.len() {
if let Some(o) =
expression_taint(arg_nodes[param_idx], ctx, state)
{
return Some(TaintOrigin {
description: format!(
"{} (via cross-file {func_name})",
o.description
),
line: o.line,
source_range: o.source_range,
});
}
}
}
}
}
}
}
}
}
}
None
}
fn is_sanitizer_call(
call_node: Node<'_>,
source: &str,
spec: &TaintSpec,
aliases: Option<&AliasTable>,
) -> bool {
if call_node.kind() != "call" {
return false;
}
let Some(func) = call_node.child_by_field_name("function") else {
return false;
};
let callee_text = node_text(func, source);
let resolved: std::borrow::Cow<'_, str> = match aliases {
Some(a) => a.resolve(callee_text),
None => std::borrow::Cow::Borrowed(callee_text),
};
for matcher in &spec.sanitizers {
if let NodeMatcher::Call { canonical, .. } = matcher {
if callee_text == canonical.as_str() || resolved.as_ref() == canonical.as_str() {
return true;
}
}
}
false
}
fn subscript_base_matches(value: Node<'_>, source: &str, want: Option<&str>) -> bool {
let Some(want) = want else {
return true;
};
match value.kind() {
"identifier" => node_text(value, source) == want,
"attribute" => value
.child_by_field_name("attribute")
.map(|a| node_text(a, source) == want)
.unwrap_or(false),
_ => false,
}
}
fn literal_matches_source_regex(node: Node<'_>, source: &str, regex: Option<&str>) -> bool {
let Some(pattern) = regex else {
return true;
};
let text = node_text(node, source);
SOURCE_REGEX_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
if let Some(re) = cache.get(pattern) {
return re.is_match(text);
}
match crate::rules::semgrep_compat::compile_regex(pattern) {
Ok(re) => {
let matched = re.is_match(text);
cache.insert(pattern.to_string(), re);
matched
}
Err(_) => false,
}
})
}
thread_local! {
static SOURCE_REGEX_CACHE: std::cell::RefCell<
HashMap<String, crate::rules::semgrep_compat::CompiledRegex>,
> = std::cell::RefCell::new(HashMap::new());
}
fn match_source(
node: Node<'_>,
source: &str,
spec: &TaintSpec,
aliases: Option<&AliasTable>,
) -> Option<String> {
for matcher in &spec.sources {
match matcher {
NodeMatcher::Attribute {
root,
field,
description,
} => {
if node.kind() != "attribute" {
continue;
}
let Some(final_attr) = node.child_by_field_name("attribute") else {
continue;
};
if node_text(final_attr, source) != field.as_str() {
continue;
}
let Some(raw_root) = leftmost_identifier(node, source) else {
continue;
};
if raw_root == root.as_str() {
return Some(description.clone());
}
if let Some(a) = aliases {
if a.resolve(raw_root).as_ref() == root.as_str() {
return Some(description.clone());
}
}
}
NodeMatcher::Call {
canonical,
description,
} => {
if node.kind() != "call" {
continue;
}
let Some(func) = node.child_by_field_name("function") else {
continue;
};
let callee_text = node_text(func, source);
if callee_text == canonical.as_str() {
return Some(description.clone());
}
if let Some(a) = aliases {
if a.resolve(callee_text).as_ref() == canonical.as_str() {
return Some(description.clone());
}
}
}
NodeMatcher::FieldName { field, description } => {
if node.kind() != "attribute" {
continue;
}
let Some(final_attr) = node.child_by_field_name("attribute") else {
continue;
};
if node_text(final_attr, source) == field.as_str() {
return Some(description.clone());
}
}
NodeMatcher::Subscript { base, description } => {
if node.kind() != "subscript" {
continue;
}
let Some(value) = node.child_by_field_name("value") else {
continue;
};
if subscript_base_matches(value, source, base.as_deref()) {
return Some(description.clone());
}
}
NodeMatcher::ParamName { .. } => {
}
NodeMatcher::DecoratedParamSource { .. } => {
}
NodeMatcher::LiteralString { description, regex } => {
if matches!(node.kind(), "string" | "concatenated_string")
&& literal_matches_source_regex(node, source, regex.as_deref())
{
return Some(description.clone());
}
}
NodeMatcher::BinopFormat { description } => {
if node_is_python_string_construction(node, source) {
return Some(description.clone());
}
}
NodeMatcher::MethodName { .. }
| NodeMatcher::CallRegex { .. }
| NodeMatcher::MethodNameRegex { .. }
| NodeMatcher::ReceiverCall { .. }
| NodeMatcher::MemberAssign { .. }
| NodeMatcher::ObjectLiteralValue { .. }
| NodeMatcher::ReturnValue { .. }
| NodeMatcher::TypedName { .. }
| NodeMatcher::TypedAssignTarget { .. }
| NodeMatcher::LooseEquality { .. }
| NodeMatcher::TaintedCallee { .. }
| NodeMatcher::TaintedSubscriptKey { .. }
| NodeMatcher::CallArgSource { .. }
| NodeMatcher::FirstParamSource { .. }
| NodeMatcher::CallArgConcat { .. }
| NodeMatcher::ConstructorArgSink { .. }
| NodeMatcher::PropertyAssignSink { .. }
| NodeMatcher::MethodArgSink { .. }
| NodeMatcher::ReceiverProvenanceCall { .. }
| NodeMatcher::LiteralArgCall { .. } => {
}
}
}
None
}
pub fn python_taint_sources() -> Vec<NodeMatcher> {
vec![
NodeMatcher::Attribute {
root: "request".into(),
field: "data".into(),
description: "flask.request.data".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "form".into(),
description: "flask.request.form".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "args".into(),
description: "flask.request.args".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "values".into(),
description: "flask.request.values".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "json".into(),
description: "flask.request.json".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "files".into(),
description: "flask.request.files".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "cookies".into(),
description: "flask.request.cookies".into(),
},
NodeMatcher::Call {
canonical: "request.get_data".into(),
description: "flask.request.get_data()".into(),
},
NodeMatcher::Call {
canonical: "request.get_json".into(),
description: "flask.request.get_json()".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "POST".into(),
description: "django.request.POST".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "GET".into(),
description: "django.request.GET".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "COOKIES".into(),
description: "django.request.COOKIES".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "FILES".into(),
description: "django.request.FILES".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "META".into(),
description: "django.request.META".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "headers".into(),
description: "django/fastapi.request.headers".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "body".into(),
description: "django/fastapi.request.body".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "query_params".into(),
description: "fastapi/starlette.request.query_params".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "path_params".into(),
description: "fastapi/starlette.request.path_params".into(),
},
NodeMatcher::Call {
canonical: "request.body".into(),
description: "fastapi/starlette.request.body()".into(),
},
NodeMatcher::Call {
canonical: "request.json".into(),
description: "fastapi/starlette.request.json()".into(),
},
NodeMatcher::Call {
canonical: "request.form".into(),
description: "fastapi/starlette.request.form()".into(),
},
NodeMatcher::Call {
canonical: "request.stream".into(),
description: "fastapi/starlette.request.stream()".into(),
},
NodeMatcher::Attribute {
root: "sys".into(),
field: "argv".into(),
description: "sys.argv".into(),
},
NodeMatcher::Call {
canonical: "sys.stdin.read".into(),
description: "sys.stdin.read()".into(),
},
NodeMatcher::Call {
canonical: "sys.stdin.readline".into(),
description: "sys.stdin.readline()".into(),
},
NodeMatcher::Call {
canonical: "input".into(),
description: "input()".into(),
},
NodeMatcher::Attribute {
root: "os".into(),
field: "environ".into(),
description: "os.environ".into(),
},
NodeMatcher::Call {
canonical: "os.getenv".into(),
description: "os.getenv(...)".into(),
},
NodeMatcher::Call {
canonical: "os.environ.get".into(),
description: "os.environ.get(...)".into(),
},
NodeMatcher::Call {
canonical: "self.get_argument".into(),
description: "tornado.self.get_argument()".into(),
},
NodeMatcher::Call {
canonical: "self.get_body_argument".into(),
description: "tornado.self.get_body_argument()".into(),
},
NodeMatcher::Call {
canonical: "self.get_query_argument".into(),
description: "tornado.self.get_query_argument()".into(),
},
NodeMatcher::Attribute {
root: "self".into(),
field: "body".into(),
description: "tornado.self.request.body".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "params".into(),
description: "bottle.request.params".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "forms".into(),
description: "bottle.request.forms".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "query".into(),
description: "bottle.request.query".into(),
},
NodeMatcher::ParamName {
names: vec!["request".into(), "req".into()],
description: "untrusted request parameter".into(),
},
]
}
fn leftmost_identifier<'a>(mut node: Node<'_>, source: &'a str) -> Option<&'a str> {
loop {
match node.kind() {
"identifier" => return Some(node_text(node, source)),
"attribute" => {
node = node.child_by_field_name("object")?;
}
_ => return None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::parser::parse_file;
use crate::rules::python_aliases::from_tree as py_aliases_from_tree;
use crate::Language;
fn spec_pickle_from_request() -> TaintSpec {
TaintSpec {
sources: vec![
NodeMatcher::Attribute {
root: "request".into(),
field: "data".into(),
description: "flask.request.data".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "form".into(),
description: "flask.request.form".into(),
},
NodeMatcher::Attribute {
root: "request".into(),
field: "args".into(),
description: "flask.request.args".into(),
},
NodeMatcher::Call {
canonical: "request.get_json".into(),
description: "flask.request.get_json()".into(),
},
NodeMatcher::ParamName {
names: vec!["request".into()],
description: "function parameter named request".into(),
},
],
sinks: vec![
NodeMatcher::Call {
canonical: "pickle.loads".into(),
description: "pickle.loads".into(),
},
NodeMatcher::Call {
canonical: "pickle.load".into(),
description: "pickle.load".into(),
},
],
sanitizers: vec![],
}
}
fn run(source: &str) -> Vec<TaintFinding> {
let tree = parse_file(source, Language::Python).expect("parse");
let aliases = py_aliases_from_tree(source, &tree);
analyze_tree(
tree.root_node(),
source,
&spec_pickle_from_request(),
Some(&aliases),
)
}
#[test]
fn direct_flow_request_data_to_pickle_loads() {
let src = r#"
import pickle
from flask import request
def handler():
data = request.data
return pickle.loads(data)
"#;
let f = run(src);
assert_eq!(f.len(), 1);
assert!(f[0].source_description.contains("request.data"));
assert_eq!(f[0].sink_description, "pickle.loads");
}
#[test]
fn direct_in_function_flow_is_tagged_one_hop() {
let src = r#"
import pickle
from flask import request
def handler():
return pickle.loads(request.data)
"#;
let f = run(src);
assert_eq!(f.len(), 1);
assert_eq!(f[0].hops, 1);
}
#[test]
fn chained_assignment_propagates_taint() {
let src = r#"
import pickle
from flask import request
def handler():
a = request.form
b = a
c = b
return pickle.loads(c)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn reassignment_to_literal_kills_taint() {
let src = r#"
import pickle
from flask import request
def handler():
data = request.data
data = b"static"
return pickle.loads(data)
"#;
assert_eq!(run(src).len(), 0);
}
#[test]
fn taint_survives_branch_over_approximation() {
let src = r#"
import pickle
from flask import request
def handler(cond):
if cond:
data = request.data
return pickle.loads(data)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn function_parameter_named_request_is_tainted() {
let src = r#"
import pickle
def handler(request):
return pickle.loads(request.data)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn nested_function_has_independent_taint() {
let src = r#"
import pickle
from flask import request
def outer():
data = request.data
def inner():
return pickle.loads(data)
return inner
"#;
assert_eq!(run(src).len(), 0);
}
#[test]
fn no_source_no_finding() {
let src = r#"
import pickle
def handler():
data = b"trusted"
return pickle.loads(data)
"#;
assert_eq!(run(src).len(), 0);
}
#[test]
fn source_call_get_json_flows_to_sink() {
let src = r#"
import pickle
from flask import request
def handler():
payload = request.get_json()
return pickle.loads(payload)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn direct_source_as_sink_argument_without_intermediate() {
let src = r#"
import pickle
from flask import request
def handler():
return pickle.loads(request.data)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn subscript_on_tainted_root_is_tainted() {
let src = r#"
import pickle
from flask import request
def handler():
form = request.form
return pickle.loads(form["payload"])
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn wrapping_call_preserves_taint() {
let src = r#"
import pickle
from flask import request
def handler():
return pickle.loads(bytes(request.data))
"#;
assert_eq!(run(src).len(), 1);
}
fn spec_pickle_with_html_escape_sanitizer() -> TaintSpec {
let mut spec = spec_pickle_from_request();
spec.sanitizers = vec![NodeMatcher::Call {
canonical: "html.escape".into(),
description: "html.escape".into(),
}];
spec
}
fn run_with(source: &str, spec: &TaintSpec) -> Vec<TaintFinding> {
let tree = parse_file(source, Language::Python).expect("parse");
let aliases = py_aliases_from_tree(source, &tree);
analyze_tree(tree.root_node(), source, spec, Some(&aliases))
}
#[test]
fn sanitizer_call_kills_taint() {
let src = r#"
import pickle
import html
from flask import request
def handler():
raw = request.data
clean = html.escape(raw)
return pickle.loads(clean)
"#;
assert_eq!(
run_with(src, &spec_pickle_with_html_escape_sanitizer()).len(),
0
);
}
#[test]
fn sanitizer_bypassed_still_flows() {
let src = r#"
import pickle
import html
from flask import request
def handler():
raw = request.data
escaped = html.escape(raw)
return pickle.loads(raw)
"#;
assert_eq!(
run_with(src, &spec_pickle_with_html_escape_sanitizer()).len(),
1
);
}
#[test]
fn non_sanitizer_wrapping_call_preserves_taint() {
let src = r#"
import pickle
from flask import request
def handler():
data = bytes(request.data)
return pickle.loads(data)
"#;
assert_eq!(
run_with(src, &spec_pickle_with_html_escape_sanitizer()).len(),
1
);
}
#[test]
fn sanitizer_result_assigned_to_new_variable() {
let src = r#"
import pickle
import html
from flask import request
def handler():
data = html.escape(request.args["q"])
return pickle.loads(data)
"#;
assert_eq!(
run_with(src, &spec_pickle_with_html_escape_sanitizer()).len(),
0
);
}
#[test]
fn multiple_sanitizers_in_spec() {
let mut spec = spec_pickle_from_request();
spec.sanitizers = vec![
NodeMatcher::Call {
canonical: "html.escape".into(),
description: "html.escape".into(),
},
NodeMatcher::Call {
canonical: "shlex.quote".into(),
description: "shlex.quote".into(),
},
];
let src_escape = r#"
import pickle
import html
from flask import request
def handler():
return pickle.loads(html.escape(request.data))
"#;
let src_quote = r#"
import pickle
import shlex
from flask import request
def handler():
return pickle.loads(shlex.quote(request.data))
"#;
let src_neither = r#"
import pickle
from flask import request
def handler():
return pickle.loads(urllib.parse.quote(request.data))
"#;
assert_eq!(run_with(src_escape, &spec).len(), 0);
assert_eq!(run_with(src_quote, &spec).len(), 0);
assert_eq!(run_with(src_neither, &spec).len(), 1);
}
#[test]
fn nested_subscript_propagates_through_chain() {
let src = r#"
import pickle
from flask import request
def handler():
return pickle.loads(request.form["a"]["b"]["c"])
"#;
let f = run(src);
assert_eq!(f.len(), 1);
assert!(f[0].source_description.contains("request.form"));
}
#[test]
fn tuple_unpack_literal_rhs_taints_matching_element() {
let src = r#"
import pickle
from flask import request
def handler():
a, b = request.args["a"], request.args["b"]
return pickle.loads(a)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn tuple_unpack_literal_rhs_leaves_clean_element_clean() {
let src = r#"
import pickle
from flask import request
def handler():
a, b = request.args["a"], b"static"
return pickle.loads(b)
"#;
assert_eq!(run(src).len(), 0);
}
#[test]
fn tuple_unpack_tainted_rhs_conservatively_taints_all_targets() {
let src = r#"
import pickle
from flask import request
def handler():
a, b = request.get_json()
return pickle.loads(b)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn list_unpack_similar_to_tuple_unpack() {
let src = r#"
import pickle
from flask import request
def handler():
[a, b] = [request.args["a"], b"static"]
return pickle.loads(a)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn interprocedural_tainted_return_propagates_to_caller() {
let src = r#"
import pickle
from flask import request
def get_user_input():
return request.data
def handler():
data = get_user_input()
return pickle.loads(data)
"#;
let f = run(src);
assert_eq!(f.len(), 1);
assert!(f[0].source_description.contains("get_user_input"));
assert!(f[0].source_description.contains("request.data"));
}
#[test]
fn interprocedural_clean_return_does_not_fire() {
let src = r#"
import pickle
def literal_helper():
return b"static"
def handler():
return pickle.loads(literal_helper())
"#;
assert_eq!(run(src).len(), 0);
}
#[test]
fn interprocedural_return_is_parameter_sensitive() {
let src = r#"
import pickle
from flask import request
def choose(first, second):
return second
def handler():
data = choose(request.data, b"static")
return pickle.loads(data)
"#;
assert_eq!(run(src).len(), 0);
}
#[test]
fn interprocedural_late_definition_still_found() {
let src = r#"
import pickle
from flask import request
def handler():
data = helper()
return pickle.loads(data)
def helper():
return request.data
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn multi_hop_chain_is_out_of_scope_v1() {
let src = r#"
import pickle
from flask import request
def source():
return request.data
def middle():
return source()
def handler():
return pickle.loads(middle())
"#;
assert_eq!(run(src).len(), 0);
}
#[test]
fn interprocedural_direct_call_as_sink_argument() {
let src = r#"
import pickle
from flask import request
def get_user_input():
return request.data
def handler():
return pickle.loads(get_user_input())
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn method_call_on_tainted_source_propagates() {
let src = r#"
import pickle
from flask import request
def handler():
data = request.args.get("x")
return pickle.loads(data)
"#;
let f = run(src);
assert_eq!(f.len(), 1);
assert!(f[0].source_description.contains("request.args"));
}
#[test]
fn method_call_on_tainted_subscript_propagates() {
let src = r#"
import pickle
from flask import request
def handler():
data = request.args["x"].upper()
return pickle.loads(data)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn method_call_on_literal_root_is_clean() {
let src = r#"
import pickle
def handler():
data = "literal".upper()
return pickle.loads(data)
"#;
assert_eq!(run(src).len(), 0);
}
#[test]
fn method_call_with_args_still_tainted() {
let src = r#"
import pickle
from flask import request
def handler():
data = request.args.get("x", "default")
return pickle.loads(data)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn chained_method_calls_preserve_taint() {
let src = r#"
import pickle
from flask import request
def handler():
data = request.args.get("x").strip().upper()
return pickle.loads(data)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn fstring_with_tainted_interpolation_is_tainted() {
let src = r#"
import pickle
from flask import request
def handler():
data = f"{request.data}"
return pickle.loads(data)
"#;
let f = run(src);
assert_eq!(f.len(), 1);
assert!(f[0].source_description.contains("request.data"));
}
#[test]
fn fstring_with_literal_only_is_clean() {
let src = r#"
import pickle
def handler():
data = f"hello {1+2}"
return pickle.loads(data)
"#;
assert_eq!(run(src).len(), 0);
}
#[test]
fn fstring_with_tainted_mixed_with_literals_is_tainted() {
let src = r#"
import pickle
from flask import request
def handler():
x = request.args["q"]
data = f"a {x} b"
return pickle.loads(data)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn alias_resolution_through_import_table() {
let src = r#"
import pickle as p
from flask import request
def handler():
return p.loads(request.data)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn string_concat_with_tainted_right_operand_is_tainted() {
let src = r#"
import pickle
from flask import request
def handler():
data = "prefix " + request.data
return pickle.loads(data)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn string_concat_with_tainted_left_operand_is_tainted() {
let src = r#"
import pickle
from flask import request
def handler():
data = request.data + " suffix"
return pickle.loads(data)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn chained_string_concat_with_tainted_operand_is_tainted() {
let src = r#"
import pickle
from flask import request
def handler():
data = "a" + "b" + request.data
return pickle.loads(data)
"#;
assert_eq!(run(src).len(), 1);
}
#[test]
fn string_concat_with_both_literal_is_clean() {
let src = r#"
import pickle
def handler():
data = "a" + "b"
return pickle.loads(data)
"#;
assert_eq!(run(src).len(), 0);
}
#[test]
fn integer_arithmetic_is_clean() {
let src = r#"
import pickle
def handler():
x = 1 + 2
data = b"trusted" + bytes([x])
return pickle.loads(data)
"#;
assert_eq!(run(src).len(), 0);
}
#[test]
fn percent_format_with_tainted_operand_is_tainted() {
let src = r#"
import pickle
from flask import request
def handler():
name = request.args.get("name")
data = "prefix_%s" % name
pickle.loads(data)
"#;
assert!(!run(src).is_empty());
}
#[test]
fn percent_format_with_clean_operand_is_clean() {
let src = r#"
import pickle
def handler():
data = "prefix_%s" % "literal"
pickle.loads(data)
"#;
assert_eq!(run(src).len(), 0);
}
#[test]
fn dot_format_with_tainted_argument_is_tainted() {
let src = r#"
import pickle
from flask import request
def handler():
name = request.args.get("name")
data = "prefix_{}".format(name)
pickle.loads(data)
"#;
assert!(!run(src).is_empty());
}
#[test]
fn dot_format_with_clean_arguments_is_clean() {
let src = r#"
import pickle
def handler():
data = "prefix_{}".format("literal")
pickle.loads(data)
"#;
assert_eq!(run(src).len(), 0);
}
#[test]
fn percent_format_tuple_with_tainted_is_tainted() {
let src = r#"
import pickle
from flask import request
def handler():
user_input = request.args.get("q")
data = "%s_%s" % ("safe", user_input)
pickle.loads(data)
"#;
assert!(!run(src).is_empty());
}
#[test]
fn conditional_expression_tainted_body_propagates() {
let src = r#"
import pickle
from flask import request
def handler():
data = request.data if True else "safe"
pickle.loads(data)
"#;
let f = run(src);
assert_eq!(f.len(), 1);
assert!(f[0].source_description.contains("request.data"));
}
#[test]
fn conditional_expression_tainted_alternative_propagates() {
let src = r#"
import pickle
from flask import request
def handler():
data = "safe" if True else request.data
pickle.loads(data)
"#;
let f = run(src);
assert_eq!(f.len(), 1);
assert!(f[0].source_description.contains("request.data"));
}
#[test]
fn conditional_expression_clean_both_branches_is_clean() {
let src = r#"
import pickle
from flask import request
def handler():
data = "a" if True else "b"
pickle.loads(data)
"#;
assert!(run(src).is_empty());
}
#[test]
fn list_comprehension_tainted_iterable_propagates() {
let src = r#"
import pickle
from flask import request
def handler():
user_input = request.args.get("q")
data = [x for x in user_input]
pickle.loads(data)
"#;
let f = run(src);
assert_eq!(f.len(), 1);
}
#[test]
fn list_comprehension_method_call_on_tainted_elements() {
let src = r#"
import pickle
from flask import request
def handler():
items = request.args.getlist("items")
data = [x.strip() for x in items]
pickle.loads(data)
"#;
let f = run(src);
assert_eq!(f.len(), 1);
}
#[test]
fn list_comprehension_clean_iterable_is_clean() {
let src = r#"
import pickle
def handler():
data = [x for x in ["safe", "literal"]]
pickle.loads(data)
"#;
assert!(run(src).is_empty());
}
#[test]
fn dict_comprehension_tainted_values_propagates() {
let src = r#"
import pickle
from flask import request
def handler():
user_input = request.args.get("q")
data = {k: v for k, v in user_input.items()}
pickle.loads(data)
"#;
let f = run(src);
assert_eq!(f.len(), 1);
}
#[test]
fn set_comprehension_tainted_iterable_propagates() {
let src = r#"
import pickle
from flask import request
def handler():
user_input = request.args.get("q")
data = {x for x in user_input}
pickle.loads(data)
"#;
let f = run(src);
assert_eq!(f.len(), 1);
}
#[test]
fn generator_expression_tainted_iterable_propagates() {
let src = r#"
import pickle
from flask import request
def handler():
user_input = request.args.get("q")
data = list(x for x in user_input)
pickle.loads(data)
"#;
let f = run(src);
assert_eq!(f.len(), 1);
}
#[test]
fn with_statement_tainted_context_propagates_to_alias() {
let src = r#"
import pickle
from flask import request
def handler():
with open(request.data) as f:
pickle.loads(f.read())
"#;
let f = run(src);
assert_eq!(f.len(), 1);
assert_eq!(f[0].sink_description, "pickle.loads");
}
#[test]
fn with_statement_clean_context_does_not_taint_alias() {
let src = r#"
import pickle
def handler():
with open("literal.txt") as f:
pickle.loads(f.read())
"#;
assert!(run(src).is_empty());
}
fn run_full_sources(source: &str) -> Vec<TaintFinding> {
let spec = TaintSpec {
sources: python_taint_sources(),
sinks: vec![NodeMatcher::Call {
canonical: "pickle.loads".into(),
description: "pickle.loads".into(),
}],
sanitizers: vec![],
};
run_with(source, &spec)
}
#[test]
fn tornado_get_argument_is_tainted() {
let src = r#"
import pickle
class Handler:
def post(self):
data = self.get_argument("payload")
pickle.loads(data)
"#;
let f = run_full_sources(src);
assert_eq!(f.len(), 1);
assert!(f[0].source_description.contains("get_argument"));
}
#[test]
fn tornado_get_body_argument_is_tainted() {
let src = r#"
import pickle
class Handler:
def post(self):
data = self.get_body_argument("payload")
pickle.loads(data)
"#;
let f = run_full_sources(src);
assert_eq!(f.len(), 1);
assert!(f[0].source_description.contains("get_body_argument"));
}
#[test]
fn tornado_get_query_argument_is_tainted() {
let src = r#"
import pickle
class Handler:
def get(self):
data = self.get_query_argument("q")
pickle.loads(data)
"#;
let f = run_full_sources(src);
assert_eq!(f.len(), 1);
assert!(f[0].source_description.contains("get_query_argument"));
}
#[test]
fn tornado_request_body_is_tainted() {
let src = r#"
import pickle
class Handler:
def post(self):
data = self.request.body
pickle.loads(data)
"#;
let f = run_full_sources(src);
assert_eq!(f.len(), 1);
}
#[test]
fn bottle_request_params_is_tainted() {
let src = r#"
import pickle
from bottle import request
def handler():
data = request.params
pickle.loads(data)
"#;
let f = run_full_sources(src);
assert_eq!(f.len(), 1);
assert!(f[0].source_description.contains("params"));
}
#[test]
fn bottle_request_forms_is_tainted() {
let src = r#"
import pickle
from bottle import request
def handler():
data = request.forms
pickle.loads(data)
"#;
let f = run_full_sources(src);
assert_eq!(f.len(), 1);
assert!(f[0].source_description.contains("forms"));
}
#[test]
fn bottle_request_query_is_tainted() {
let src = r#"
import pickle
from bottle import request
def handler():
data = request.query
pickle.loads(data)
"#;
let f = run_full_sources(src);
assert_eq!(f.len(), 1);
assert!(f[0].source_description.contains("query"));
}
#[test]
fn walrus_operator_propagates_taint() {
let src = r#"
import pickle
from flask import request
def handler():
if data := request.get_json():
pickle.loads(data)
"#;
let f = run(src);
assert_eq!(f.len(), 1);
assert_eq!(f[0].sink_description, "pickle.loads");
}
}