use crate::rules::common::AliasTable;
use crate::rules::cross_file::{CrossFileSummaryMap, FunctionTaintSummary, ParamSinkFlow};
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 current_rule_id: &'a str,
}
#[derive(Debug, Clone)]
pub enum NodeMatcher {
Attribute {
root: String,
field: String,
description: String,
},
Call {
canonical: String,
description: String,
},
ParamName {
names: Vec<String>,
description: String,
},
MethodName { method: String, description: String },
}
impl NodeMatcher {
pub fn description(&self) -> &str {
match self {
NodeMatcher::Attribute { description, .. } => description,
NodeMatcher::Call { description, .. } => description,
NodeMatcher::ParamName { description, .. } => description,
NodeMatcher::MethodName { description, .. } => description,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TaintSpec {
pub sources: Vec<NodeMatcher>,
pub sinks: Vec<NodeMatcher>,
pub sanitizers: Vec<NodeMatcher>,
}
#[derive(Debug, Clone)]
pub struct TaintFinding {
pub sink_start_byte: usize,
pub sink_end_byte: usize,
pub sink_line: usize,
pub sink_column: usize,
pub sink_end_line: usize,
pub sink_end_column: usize,
pub source_description: String,
pub sink_description: String,
pub source_line: usize,
}
pub type ReturnSummary = HashMap<String, Option<String>>;
struct AnalysisContext<'a> {
source: &'a str,
spec: &'a TaintSpec,
aliases: Option<&'a AliasTable>,
summaries: &'a ReturnSummary,
cross_file: Option<&'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,
};
collect_function_defs(root, &mut |func_node| {
let (name, ret_taint) = summarize_function(func_node, &pass1_ctx);
if let Some(name) = name {
summaries.insert(name, ret_taint);
}
});
let ctx = AnalysisContext {
source,
spec,
aliases,
summaries: &summaries,
cross_file,
};
let mut findings = Vec::new();
collect_function_defs(root, &mut |func_node| {
analyze_function(func_node, &ctx, &mut findings);
});
findings
}
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 param_names.is_empty() {
return;
}
let mut params_to_sink: Vec<ParamSinkFlow> = Vec::new();
let mut params_to_return: Vec<usize> = Vec::new();
for (param_idx, param_name) in param_names.iter().enumerate() {
let synthetic_source = NodeMatcher::ParamName {
names: vec![param_name.clone()],
description: format!("parameter '{}'", param_name),
};
let return_spec = TaintSpec {
sources: vec![synthetic_source.clone()],
sinks: vec![],
sanitizers: vec![],
};
let empty_summary = ReturnSummary::new();
let return_ctx = AnalysisContext {
source,
spec: &return_spec,
aliases,
summaries: &empty_summary,
cross_file: None,
};
let (_, ret_taint) = summarize_function(func_node, &return_ctx);
if ret_taint.is_some() && !params_to_return.contains(¶m_idx) {
params_to_return.push(param_idx);
}
for (rule_id, rule_spec) in rule_specs {
let synthetic_spec = TaintSpec {
sources: vec![synthetic_source.clone()],
sinks: rule_spec.sinks.clone(),
sanitizers: rule_spec.sanitizers.clone(),
};
let sink_ctx = AnalysisContext {
source,
spec: &synthetic_spec,
aliases,
summaries: &empty_summary,
cross_file: None,
};
let mut findings = Vec::new();
analyze_function(func_node, &sink_ctx, &mut findings);
if !findings.is_empty() {
let already = params_to_sink
.iter()
.any(|f| f.param_index == param_idx && f.sink_rule_id == *rule_id);
if !already {
params_to_sink.push(ParamSinkFlow {
param_index: param_idx,
sink_rule_id: rule_id.to_string(),
sink_description: findings[0].sink_description.clone(),
});
}
}
}
}
if !params_to_sink.is_empty() || !params_to_return.is_empty() {
summaries.push(FunctionTaintSummary {
name: func_name,
params_to_return,
params_to_sink,
});
}
});
summaries
}
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 summarize_function(
func_node: Node<'_>,
ctx: &AnalysisContext<'_>,
) -> (Option<String>, Option<String>) {
let name = func_node
.child_by_field_name("name")
.map(|n| node_text(n, ctx.source).to_string());
let mut state = TaintState::default();
if let Some(params) = func_node.child_by_field_name("parameters") {
seed_param_sources(params, ctx.source, ctx.spec, &mut state);
}
let Some(body) = func_node.child_by_field_name("body") else {
return (name, None);
};
let mut return_taint: Option<String> = None;
let mut scratch: Vec<TaintFinding> = Vec::new();
walk_body_for_summary(body, ctx, &mut state, &mut scratch, &mut return_taint);
(name, return_taint)
}
fn walk_body_for_summary(
node: Node<'_>,
ctx: &AnalysisContext<'_>,
state: &mut TaintState,
findings: &mut Vec<TaintFinding>,
return_taint: &mut Option<String>,
) {
if node.kind() == "function_definition" {
return;
}
if node.kind() == "assignment" {
handle_assignment(node, ctx, state);
}
if node.kind() == "call" {
handle_call(node, ctx, state, findings);
}
if node.kind() == "with_statement" {
handle_with_statement(node, ctx, state);
}
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((desc, _line)) = expression_taint(child, ctx, state) {
*return_taint = Some(desc);
break;
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_body_for_summary(child, ctx, state, findings, return_taint);
}
}
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);
}
}
#[derive(Clone, Debug)]
struct TaintInfo {
description: String,
line: usize,
}
#[derive(Default)]
struct TaintState {
tainted: HashMap<String, TaintInfo>,
}
impl TaintState {
fn taint(&mut self, name: String, description: String, line: usize) {
self.tainted.insert(name, TaintInfo { description, line });
}
fn clear(&mut self, name: &str) {
self.tainted.remove(name);
}
fn info(&self, name: &str) -> Option<&TaintInfo> {
self.tainted.get(name)
}
}
fn analyze_function(
func_node: Node<'_>,
ctx: &AnalysisContext<'_>,
findings: &mut Vec<TaintFinding>,
) {
let mut state = TaintState::default();
if let Some(params) = func_node.child_by_field_name("parameters") {
seed_param_sources(params, ctx.source, ctx.spec, &mut state);
}
let Some(body) = func_node.child_by_field_name("body") else {
return;
};
walk_body(body, ctx, &mut state, findings);
}
fn seed_param_sources(params: Node<'_>, source: &str, spec: &TaintSpec, state: &mut TaintState) {
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 {
if let NodeMatcher::ParamName { names, description } = matcher {
if names.iter().any(|n| n == param_name) {
let line = child.start_position().row + 1;
state.taint(param_name.to_string(), description.clone(), line);
break;
}
}
}
}
}
fn walk_body(
node: Node<'_>,
ctx: &AnalysisContext<'_>,
state: &mut TaintState,
findings: &mut Vec<TaintFinding>,
) {
if node.kind() == "function_definition" {
return;
}
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((desc, src_line)) = expression_taint(value, ctx, state) {
state.taint(lhs, desc, src_line);
} 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);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_body(child, ctx, state, findings);
}
}
fn handle_assignment(node: Node<'_>, ctx: &AnalysisContext<'_>, 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((desc, src_line)) = expression_taint(right, ctx, state) {
state.taint(lhs_name, desc, src_line);
} 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((desc, src_line)) = expression_taint(*rhs, ctx, state) {
state.taint(target.clone(), desc, src_line);
} else {
state.clear(target);
}
}
return;
}
}
if let Some((desc, src_line)) = expression_taint(right, ctx, state) {
for target in &lhs_targets {
state.taint(target.clone(), desc.clone(), src_line);
}
} else {
for target in &lhs_targets {
state.clear(target);
}
}
}
}
fn handle_with_statement(node: Node<'_>, ctx: &AnalysisContext<'_>, 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: &AnalysisContext<'_>, 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((desc, src_line)) = expression_taint(value, ctx, state) {
state.taint(alias_name, desc, src_line);
} 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: &AnalysisContext<'_>,
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(),
};
let final_segment = resolved.rsplit('.').next().unwrap_or(resolved.as_str());
let sink_desc = ctx.spec.sinks.iter().find_map(|m| match m {
NodeMatcher::Call {
canonical,
description,
} if *canonical == resolved => Some(description.clone()),
NodeMatcher::MethodName {
method,
description,
} if method == final_segment => Some(description.clone()),
_ => None,
});
if let Some(sink_desc) = sink_desc {
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((source_desc, src_line)) = expression_taint(arg, ctx, state) {
let start = node.start_position();
let end = node.end_position();
findings.push(TaintFinding {
sink_start_byte: node.start_byte(),
sink_end_byte: node.end_byte(),
sink_line: start.row + 1,
sink_column: start.column + 1,
sink_end_line: end.row + 1,
sink_end_column: end.column + 1,
source_description: source_desc,
sink_description: sink_desc.clone(),
source_line: src_line,
});
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_cross_file_call(
node: Node<'_>,
func: Node<'_>,
callee_text: &str,
ctx: &AnalysisContext<'_>,
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 flow.sink_rule_id != cross_file.current_rule_id {
continue;
}
if flow.param_index >= arg_nodes.len() {
continue;
}
let arg = arg_nodes[flow.param_index];
if let Some((source_desc, src_line)) = expression_taint(arg, ctx, state) {
let start = node.start_position();
let end = node.end_position();
findings.push(TaintFinding {
sink_start_byte: node.start_byte(),
sink_end_byte: node.end_byte(),
sink_line: start.row + 1,
sink_column: start.column + 1,
sink_end_line: end.row + 1,
sink_end_column: end.column + 1,
source_description: source_desc,
sink_description: format!(
"{} (via cross-file call to {})",
flow.sink_description, func_name
),
source_line: src_line,
});
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
}
fn expression_taint(
expr: Node<'_>,
ctx: &AnalysisContext<'_>,
state: &TaintState,
) -> Option<(String, usize)> {
let expr_line = expr.start_position().row + 1;
if let Some(desc) = match_source(expr, ctx.source, ctx.spec, ctx.aliases) {
return Some((desc, expr_line));
}
if expr.kind() == "identifier" {
let name = node_text(expr, ctx.source);
if let Some(info) = state.info(name) {
return Some((info.description.clone(), info.line));
}
}
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((info.description.clone(), info.line));
}
}
}
}
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 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(Some(desc)) = ctx.summaries.get(callee) {
return Some((format!("{desc} (via {callee})"), expr_line));
}
}
}
}
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 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::ParamName { .. } => {
}
NodeMatcher::MethodName { .. } => {
}
}
}
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,
}
}
}
fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
&source[node.byte_range()]
}
#[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 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_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");
}
}