use crate::rules::common::{walk_tree, AliasTable};
pub use crate::rules::taint_engine::{NodeMatcher, TaintFinding, TaintSpec};
use std::collections::HashMap;
use tree_sitter::Node;
#[derive(Clone, Debug)]
struct TaintInfo {
description: String,
line: usize,
hops: u8,
}
#[derive(Default)]
struct TaintState {
tainted: HashMap<String, TaintInfo>,
}
impl TaintState {
fn taint(&mut self, name: String, info: TaintInfo) {
self.tainted.insert(name, info);
}
fn clear(&mut self, name: &str) {
self.tainted.remove(name);
}
fn info(&self, name: &str) -> Option<&TaintInfo> {
self.tainted.get(name)
}
}
pub fn analyze_tree(
root: Node<'_>,
source: &str,
spec: &TaintSpec,
_aliases: Option<&AliasTable>,
) -> Vec<TaintFinding> {
let mut findings = Vec::new();
walk_tree(root, source, &mut |node, src| {
if is_scope_node(node.kind()) {
analyze_scope(node, src, spec, &mut findings);
}
});
findings
}
pub fn csharp_taint_rule_specs() -> Vec<(&'static str, TaintSpec)> {
vec![
("csharp/taint-sql-injection", sql_injection_spec()),
("csharp/taint-command-injection", command_injection_spec()),
("csharp/taint-xss", xss_spec()),
("csharp/taint-open-redirect", open_redirect_spec()),
("csharp/taint-xxe", xxe_spec()),
("csharp/taint-unsafe-load", unsafe_load_spec()),
]
}
pub fn csharp_taint_sources() -> Vec<NodeMatcher> {
vec![
NodeMatcher::Attribute {
root: "Request".into(),
field: "QueryString".into(),
description: "Request.QueryString".into(),
},
NodeMatcher::Attribute {
root: "Request".into(),
field: "Form".into(),
description: "Request.Form".into(),
},
NodeMatcher::Attribute {
root: "Request".into(),
field: "Params".into(),
description: "Request.Params".into(),
},
NodeMatcher::Attribute {
root: "Request".into(),
field: "Cookies".into(),
description: "Request.Cookies".into(),
},
NodeMatcher::Attribute {
root: "Request".into(),
field: "Headers".into(),
description: "Request.Headers".into(),
},
NodeMatcher::Attribute {
root: "Request".into(),
field: "RawUrl".into(),
description: "Request.RawUrl".into(),
},
NodeMatcher::Attribute {
root: "Request".into(),
field: "Url".into(),
description: "Request.Url".into(),
},
NodeMatcher::Attribute {
root: "Request".into(),
field: "Path".into(),
description: "Request.Path".into(),
},
NodeMatcher::Attribute {
root: "Request".into(),
field: "UserAgent".into(),
description: "Request.UserAgent".into(),
},
NodeMatcher::Attribute {
root: "Request".into(),
field: "ServerVariables".into(),
description: "Request.ServerVariables".into(),
},
NodeMatcher::Attribute {
root: "HttpContext".into(),
field: "Request".into(),
description: "HttpContext.Request".into(),
},
NodeMatcher::Call {
canonical: "Console.ReadLine".into(),
description: "Console.ReadLine()".into(),
},
NodeMatcher::Call {
canonical: "Console.Read".into(),
description: "Console.Read()".into(),
},
NodeMatcher::Call {
canonical: "Console.ReadKey".into(),
description: "Console.ReadKey()".into(),
},
NodeMatcher::Call {
canonical: "Environment.GetEnvironmentVariable".into(),
description: "Environment.GetEnvironmentVariable()".into(),
},
NodeMatcher::Attribute {
root: "Environment".into(),
field: "GetCommandLineArgs".into(),
description: "Environment.GetCommandLineArgs()".into(),
},
]
}
pub fn csharp_taint_sanitizers() -> Vec<NodeMatcher> {
vec![
NodeMatcher::Call {
canonical: "HttpUtility.HtmlEncode".into(),
description: "HttpUtility.HtmlEncode".into(),
},
NodeMatcher::Call {
canonical: "HttpUtility.HtmlAttributeEncode".into(),
description: "HttpUtility.HtmlAttributeEncode".into(),
},
NodeMatcher::Call {
canonical: "HttpUtility.UrlEncode".into(),
description: "HttpUtility.UrlEncode".into(),
},
NodeMatcher::Call {
canonical: "HtmlEncoder.Default.Encode".into(),
description: "HtmlEncoder.Default.Encode".into(),
},
NodeMatcher::MethodName {
method: "HtmlEncode".into(),
description: "HtmlEncode".into(),
},
NodeMatcher::Call {
canonical: "SqlParameter".into(),
description: "SqlParameter (parameterized query)".into(),
},
NodeMatcher::Call {
canonical: "int.Parse".into(),
description: "int.Parse (numeric conversion)".into(),
},
NodeMatcher::Call {
canonical: "Convert.ToInt32".into(),
description: "Convert.ToInt32 (numeric conversion)".into(),
},
NodeMatcher::Call {
canonical: "Convert.ToInt64".into(),
description: "Convert.ToInt64 (numeric conversion)".into(),
},
NodeMatcher::Call {
canonical: "Path.GetFileName".into(),
description: "Path.GetFileName (path sanitizer)".into(),
},
NodeMatcher::Call {
canonical: "Path.GetFullPath".into(),
description: "Path.GetFullPath (path canonicalization)".into(),
},
]
}
fn sql_injection_spec() -> TaintSpec {
TaintSpec {
sources: csharp_taint_sources(),
sinks: vec![
NodeMatcher::Call {
canonical: "SqlCommand".into(),
description: "SqlCommand() with tainted query (SQL injection)".into(),
},
NodeMatcher::Call {
canonical: "OleDbCommand".into(),
description: "OleDbCommand() with tainted query (SQL injection)".into(),
},
NodeMatcher::Call {
canonical: "MySqlCommand".into(),
description: "MySqlCommand() with tainted query (SQL injection)".into(),
},
NodeMatcher::MethodName {
method: "ExecuteReader".into(),
description: "ExecuteReader() with tainted query (SQL injection)".into(),
},
NodeMatcher::MethodName {
method: "ExecuteNonQuery".into(),
description: "ExecuteNonQuery() with tainted query (SQL injection)".into(),
},
NodeMatcher::MethodName {
method: "ExecuteScalar".into(),
description: "ExecuteScalar() with tainted query (SQL injection)".into(),
},
NodeMatcher::MethodName {
method: "ExecuteXmlReader".into(),
description: "ExecuteXmlReader() with tainted query (SQL injection)".into(),
},
NodeMatcher::MethodName {
method: "FromSqlRaw".into(),
description: "FromSqlRaw() with tainted query (SQL injection)".into(),
},
NodeMatcher::MethodName {
method: "ExecuteSqlRaw".into(),
description: "ExecuteSqlRaw() with tainted query (SQL injection)".into(),
},
NodeMatcher::MethodName {
method: "Query".into(),
description: "Dapper.Query() with tainted SQL (SQL injection)".into(),
},
NodeMatcher::MethodName {
method: "Execute".into(),
description: "Dapper.Execute() with tainted SQL (SQL injection)".into(),
},
],
sanitizers: csharp_taint_sanitizers(),
}
}
fn command_injection_spec() -> TaintSpec {
TaintSpec {
sources: csharp_taint_sources(),
sinks: vec![
NodeMatcher::Call {
canonical: "Process.Start".into(),
description: "Process.Start() with tainted argument (command injection)".into(),
},
NodeMatcher::Call {
canonical: "ProcessStartInfo".into(),
description: "ProcessStartInfo() with tainted argument (command injection)".into(),
},
NodeMatcher::Attribute {
root: "ProcessStartInfo".into(),
field: "Arguments".into(),
description: "ProcessStartInfo.Arguments tainted (command injection)".into(),
},
NodeMatcher::Attribute {
root: "ProcessStartInfo".into(),
field: "FileName".into(),
description: "ProcessStartInfo.FileName tainted (command injection)".into(),
},
NodeMatcher::MethodName {
method: "Start".into(),
description: "Process.Start() with tainted argument (command injection)".into(),
},
],
sanitizers: csharp_taint_sanitizers(),
}
}
fn xss_spec() -> TaintSpec {
TaintSpec {
sources: csharp_taint_sources(),
sinks: vec![
NodeMatcher::Call {
canonical: "Response.Write".into(),
description: "Response.Write() with tainted content (XSS)".into(),
},
NodeMatcher::MethodName {
method: "Write".into(),
description: "Response.Write() with tainted content (XSS)".into(),
},
],
sanitizers: csharp_taint_sanitizers(),
}
}
fn open_redirect_spec() -> TaintSpec {
TaintSpec {
sources: csharp_taint_sources(),
sinks: vec![
NodeMatcher::Call {
canonical: "Response.Redirect".into(),
description: "Response.Redirect() with tainted URL (open redirect)".into(),
},
NodeMatcher::MethodName {
method: "Redirect".into(),
description: "Response.Redirect() with tainted URL (open redirect)".into(),
},
],
sanitizers: csharp_taint_sanitizers(),
}
}
fn xxe_spec() -> TaintSpec {
TaintSpec {
sources: csharp_taint_sources(),
sinks: vec![
NodeMatcher::Call {
canonical: "XmlReader.Create".into(),
description: "XmlReader.Create() with tainted input (XXE)".into(),
},
NodeMatcher::MethodName {
method: "LoadXml".into(),
description: "XmlDocument.LoadXml() with tainted input (XXE)".into(),
},
NodeMatcher::MethodName {
method: "Load".into(),
description: "XmlDocument.Load() with tainted input (XXE)".into(),
},
],
sanitizers: csharp_taint_sanitizers(),
}
}
fn unsafe_load_spec() -> TaintSpec {
TaintSpec {
sources: csharp_taint_sources(),
sinks: vec![
NodeMatcher::Call {
canonical: "Assembly.Load".into(),
description: "Assembly.Load() with tainted name (unsafe load)".into(),
},
NodeMatcher::Call {
canonical: "Assembly.LoadFrom".into(),
description: "Assembly.LoadFrom() with tainted path (unsafe load)".into(),
},
NodeMatcher::Call {
canonical: "Activator.CreateInstance".into(),
description: "Activator.CreateInstance() with tainted type (unsafe load)".into(),
},
NodeMatcher::Call {
canonical: "Type.GetType".into(),
description: "Type.GetType() with tainted type name (reflection injection)".into(),
},
],
sanitizers: csharp_taint_sanitizers(),
}
}
fn analyze_scope(
scope_node: Node<'_>,
source: &str,
spec: &TaintSpec,
out: &mut Vec<TaintFinding>,
) {
let body = find_scope_body(scope_node).unwrap_or(scope_node);
let mut state = TaintState::default();
for _ in 0..3 {
propagate_assignments(body, source, spec, &mut state);
}
find_sinks(body, source, spec, &state, out);
}
fn propagate_assignments(scope: Node<'_>, source: &str, spec: &TaintSpec, state: &mut TaintState) {
walk_scope_nodes(scope, source, &mut |node, src| {
if node.kind() == "variable_declarator" {
let Some(name_node) = node.child_by_field_name("name") else {
return;
};
let name = node_text(name_node, src).to_string();
if let Some(value) = variable_declarator_value(node) {
match expression_taint(value, src, spec, state) {
Some(info) => state.taint(name, bump_hops(info)),
None => state.clear(&name),
}
}
}
if node.kind() == "assignment_expression" {
let Some(left) = node.child_by_field_name("left") else {
return;
};
let Some(right) = node.child_by_field_name("right") else {
return;
};
let name = assignment_target_name(left, src);
if let Some(name) = name {
match expression_taint(right, src, spec, state) {
Some(info) => state.taint(name.to_string(), bump_hops(info)),
None => state.clear(name),
}
}
}
});
}
fn find_sinks(
scope: Node<'_>,
source: &str,
spec: &TaintSpec,
state: &TaintState,
out: &mut Vec<TaintFinding>,
) {
walk_scope_nodes(scope, source, &mut |node, src| {
let is_call = node.kind() == "invocation_expression";
let is_new = node.kind() == "object_creation_expression";
if !is_call && !is_new {
return;
}
let Some(sink_desc) = match_sink(node, src, spec) else {
return;
};
if let Some(info) = sink_argument_taint(node, src, spec, state) {
out.push(taint_finding_for_node(node, info, sink_desc));
}
});
}
fn expression_taint(
node: Node<'_>,
source: &str,
spec: &TaintSpec,
state: &TaintState,
) -> Option<TaintInfo> {
let text = node_text(node, source);
if let Some(info) = state.info(text) {
return Some(info.clone());
}
if node.kind() == "identifier" {
return state.info(text).cloned();
}
if is_sanitizer_call(node, source, spec) {
return None;
}
if let Some(desc) = classify_source_expr(node, source, spec) {
return Some(TaintInfo {
description: desc,
line: node.start_position().row + 1,
hops: 0,
});
}
if node.kind() == "member_access_expression" {
if let Some(recv) = node.child_by_field_name("expression") {
if let Some(info) = expression_taint(recv, source, spec, state) {
return Some(bump_hops(info));
}
}
}
if node.kind() == "element_access_expression" {
if let Some(expr) = node.child_by_field_name("expression") {
if let Some(info) = expression_taint(expr, source, spec, state) {
return Some(bump_hops(info));
}
}
}
if node.kind() == "invocation_expression" {
if let Some(args) = call_arguments(node) {
if let Some(info) = argument_list_taint(args, source, spec, state) {
return Some(bump_hops(info));
}
}
}
if node.kind() == "binary_expression" {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if !child.is_named() {
continue;
}
if let Some(info) = expression_taint(child, source, spec, state) {
return Some(info);
}
}
}
if node.kind() == "interpolated_string_expression" {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "interpolation" {
let mut icursor = child.walk();
for inner in child.children(&mut icursor) {
if let Some(info) = expression_taint(inner, source, spec, state) {
return Some(info);
}
}
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if let Some(info) = expression_taint(child, source, spec, state) {
return Some(info);
}
}
None
}
fn classify_source_expr(node: Node<'_>, source: &str, spec: &TaintSpec) -> Option<String> {
for matcher in &spec.sources {
match matcher {
NodeMatcher::Attribute {
root,
field,
description,
} => {
if node.kind() == "member_access_expression" {
let recv = node.child_by_field_name("expression");
let name_node = node.child_by_field_name("name");
if let (Some(recv), Some(name_node)) = (recv, name_node) {
let recv_text = node_text(recv, source);
let name_text = node_text(name_node, source);
if recv_text == root.as_str() && name_text == field.as_str() {
return Some(description.clone());
}
}
}
if node.kind() == "element_access_expression" {
if let Some(expr) = node.child_by_field_name("expression") {
if classify_source_expr(expr, source, spec).is_some() {
return Some(description.clone());
}
}
}
}
NodeMatcher::Call {
canonical,
description,
} => {
if node.kind() == "invocation_expression" {
if let Some(func) = node.child_by_field_name("function") {
let resolved = resolve_callee(func, source);
if resolved == canonical.as_str() {
return Some(description.clone());
}
}
}
if node.kind() == "object_creation_expression" {
if let Some(type_node) = node.child_by_field_name("type") {
if node_text(type_node, source) == canonical.as_str() {
return Some(description.clone());
}
}
}
}
NodeMatcher::ParamName { names, description } => {
if node.kind() == "identifier" {
let text = node_text(node, source);
if names.iter().any(|n| n == text) {
return Some(description.clone());
}
}
if node.kind() == "member_access_expression"
|| node.kind() == "element_access_expression"
{
if let Some(root_name) = leftmost_receiver_name(node, source) {
if names.iter().any(|n| n == root_name) {
return Some(description.clone());
}
}
}
}
NodeMatcher::FieldName { field, description } => {
if node.kind() == "member_access_expression" {
if let Some(name_node) = node.child_by_field_name("name") {
if node_text(name_node, source) == field.as_str() {
return Some(description.clone());
}
}
}
}
NodeMatcher::Subscript { base, description } => {
if node.kind() == "element_access_expression" {
if let Some(expr) = node.child_by_field_name("expression") {
match base.as_deref() {
None => return Some(description.clone()),
Some(want) => {
let final_seg = match expr.kind() {
"identifier" => Some(node_text(expr, source)),
"member_access_expression" => expr
.child_by_field_name("name")
.map(|n| node_text(n, source)),
_ => None,
};
if final_seg == Some(want) {
return Some(description.clone());
}
}
}
}
}
}
NodeMatcher::MethodName { .. }
| NodeMatcher::CallRegex { .. }
| NodeMatcher::MethodNameRegex { .. }
| NodeMatcher::ReceiverCall { .. }
| NodeMatcher::MemberAssign { .. } => {
}
NodeMatcher::BinopFormat { .. }
| NodeMatcher::ObjectLiteralValue { .. }
| NodeMatcher::ReturnValue { .. } => {
}
}
}
None
}
fn is_sanitizer_call(node: Node<'_>, source: &str, spec: &TaintSpec) -> bool {
spec.sanitizers
.iter()
.any(|matcher| matcher_matches_call(matcher, node, source))
}
fn match_sink(node: Node<'_>, source: &str, spec: &TaintSpec) -> Option<String> {
spec.sinks.iter().find_map(|matcher| {
if matcher_matches_call(matcher, node, source) {
Some(matcher.description().to_string())
} else {
None
}
})
}
fn matcher_matches_call(matcher: &NodeMatcher, node: Node<'_>, source: &str) -> bool {
match matcher {
NodeMatcher::MethodName { method, .. } => {
if node.kind() == "invocation_expression" {
if let Some(func) = node.child_by_field_name("function") {
if let Some(method_name) = final_name_segment(func, source) {
return method_name == method.as_str();
}
}
}
false
}
NodeMatcher::Call { canonical, .. } => {
if node.kind() == "invocation_expression" {
if let Some(func) = node.child_by_field_name("function") {
let resolved = resolve_callee(func, source);
return resolved == canonical.as_str();
}
}
if node.kind() == "object_creation_expression" {
if let Some(type_node) = node.child_by_field_name("type") {
return node_text(type_node, source) == canonical.as_str();
}
}
false
}
NodeMatcher::ReceiverCall { receiver, .. } => {
if node.kind() == "invocation_expression" {
if let Some(func) = node.child_by_field_name("function") {
let resolved = resolve_callee(func, source);
return resolved.contains('.')
&& resolved.split('.').next() == Some(receiver.as_str());
}
}
false
}
NodeMatcher::CallRegex { regex, .. } => {
if node.kind() == "invocation_expression" {
if let Some(func) = node.child_by_field_name("function") {
let resolved = resolve_callee(func, source);
return regex.is_match(resolved);
}
}
false
}
NodeMatcher::MethodNameRegex { regex, .. } => {
if node.kind() == "invocation_expression" {
if let Some(func) = node.child_by_field_name("function") {
if let Some(method_name) = final_name_segment(func, source) {
return regex.is_match(method_name);
}
}
}
false
}
NodeMatcher::Attribute { root, field, .. } => {
if node.kind() == "member_access_expression" {
let recv = node.child_by_field_name("expression");
let name_node = node.child_by_field_name("name");
if let (Some(recv), Some(name_node)) = (recv, name_node) {
let recv_text = node_text(recv, source);
let name_text = node_text(name_node, source);
return recv_text == root.as_str() && name_text == field.as_str();
}
}
false
}
NodeMatcher::FieldName { .. }
| NodeMatcher::Subscript { .. }
| NodeMatcher::ParamName { .. }
| NodeMatcher::MemberAssign { .. }
| NodeMatcher::BinopFormat { .. }
| NodeMatcher::ObjectLiteralValue { .. }
| NodeMatcher::ReturnValue { .. } => false,
}
}
fn sink_argument_taint(
node: Node<'_>,
source: &str,
spec: &TaintSpec,
state: &TaintState,
) -> Option<TaintInfo> {
call_arguments(node).and_then(|args| argument_list_taint(args, source, spec, state))
}
fn is_scope_node(kind: &str) -> bool {
matches!(
kind,
"method_declaration"
| "constructor_declaration"
| "local_function_statement"
| "lambda_expression"
| "anonymous_method_expression"
)
}
fn find_scope_body(node: Node<'_>) -> Option<Node<'_>> {
if let Some(body) = node.child_by_field_name("body") {
return Some(body);
}
let mut cursor = node.walk();
let result = node
.children(&mut cursor)
.find(|child| matches!(child.kind(), "block" | "arrow_expression_clause"));
result
}
fn walk_scope_nodes(scope: Node<'_>, source: &str, visitor: &mut impl FnMut(Node<'_>, &str)) {
let mut cursor = scope.walk();
for child in scope.children(&mut cursor) {
walk_scope_node(child, source, visitor);
}
}
fn walk_scope_node(node: Node<'_>, source: &str, visitor: &mut impl FnMut(Node<'_>, &str)) {
if is_scope_node(node.kind()) {
return;
}
visitor(node, source);
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_scope_node(child, source, visitor);
}
}
fn resolve_callee<'a>(func_node: Node<'a>, source: &'a str) -> &'a str {
node_text(func_node, source)
}
fn final_name_segment<'a>(func_node: Node<'a>, source: &'a str) -> Option<&'a str> {
if func_node.kind() == "member_access_expression" {
return func_node
.child_by_field_name("name")
.map(|n| node_text(n, source));
}
if func_node.kind() == "identifier" {
return Some(node_text(func_node, source));
}
None
}
fn leftmost_receiver_name<'a>(node: Node<'_>, source: &'a str) -> Option<&'a str> {
match node.kind() {
"identifier" => Some(node_text(node, source)),
"member_access_expression" => {
if let Some(recv) = node.child_by_field_name("expression") {
leftmost_receiver_name(recv, source)
} else {
None
}
}
"element_access_expression" => {
if let Some(recv) = node.child_by_field_name("expression") {
leftmost_receiver_name(recv, source)
} else {
None
}
}
_ => None,
}
}
fn variable_declarator_value(node: Node<'_>) -> Option<Node<'_>> {
let count = node.child_count();
if count < 3 {
return None;
}
let last = node.child(count - 1)?;
if last.kind() == "=" {
return None;
}
Some(last)
}
fn call_arguments(node: Node<'_>) -> Option<Node<'_>> {
if node.kind() == "invocation_expression" || node.kind() == "object_creation_expression" {
return node.child_by_field_name("arguments");
}
None
}
fn argument_list_taint(
arg_list: Node<'_>,
source: &str,
spec: &TaintSpec,
state: &TaintState,
) -> Option<TaintInfo> {
let mut cursor = arg_list.walk();
for child in arg_list.children(&mut cursor) {
if child.kind() == "argument" {
let mut acursor = child.walk();
for expr in child.children(&mut acursor) {
if expr.is_named() {
if let Some(info) = expression_taint(expr, source, spec, state) {
return Some(info);
}
}
}
} else if child.is_named() {
if let Some(info) = expression_taint(child, source, spec, state) {
return Some(info);
}
}
}
None
}
fn assignment_target_name<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
match node.kind() {
"identifier" => Some(node_text(node, source)),
"member_access_expression" => Some(node_text(node, source)),
_ => None,
}
}
fn bump_hops(mut info: TaintInfo) -> TaintInfo {
info.hops = info.hops.saturating_add(1);
info
}
fn taint_finding_for_node(
node: Node<'_>,
source_info: TaintInfo,
sink_description: String,
) -> TaintFinding {
let start = node.start_position();
let end = node.end_position();
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_info.description,
sink_description,
source_line: source_info.line,
rule_id_hint: None,
hops: source_info.hops.max(1),
}
}
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::Language;
fn analyze(src: &str, spec: &TaintSpec) -> Vec<TaintFinding> {
let Some(tree) = parse_file(src, Language::CSharp) else {
panic!("C# fixture should parse");
};
analyze_tree(tree.root_node(), src, spec, None)
}
#[test]
#[ignore]
fn dump_ast_for_debug() {
let src = r#"
class Controller {
public void Handle() {
string cmd = Request.QueryString["cmd"];
Process.Start(cmd);
}
}
"#;
let Some(tree) = parse_file(src, Language::CSharp) else {
panic!("should parse");
};
fn dump(node: tree_sitter::Node, source: &str, depth: usize) {
let indent = " ".repeat(depth);
let text = &source[node.byte_range()];
let text_short: String = text.chars().take(50).collect();
let mut cursor = node.walk();
let has_fields = cursor.goto_first_child();
if has_fields {
loop {
let field_name = cursor.field_name().unwrap_or("<anon>");
eprintln!(
"{}{}.{} = {:?}",
indent,
node.kind(),
field_name,
cursor.node().kind()
);
if !cursor.goto_next_sibling() {
break;
}
}
}
eprintln!("{}{} = {:?}", indent, node.kind(), text_short);
let mut c = node.walk();
for child in node.children(&mut c) {
dump(child, source, depth + 1);
}
}
dump(tree.root_node(), src, 0);
panic!("dump complete — check stderr");
}
#[test]
fn command_injection_request_querystring_to_process_start() {
let src = r#"
using System.Diagnostics;
using System.Web;
class Controller {
public void Handle() {
string cmd = Request.QueryString["cmd"];
Process.Start(cmd);
}
}
"#;
let findings = analyze(src, &command_injection_spec());
assert!(
!findings.is_empty(),
"should detect Request.QueryString -> Process.Start: {findings:?}"
);
}
#[test]
fn sql_injection_request_form_to_execute_reader() {
let src = r#"
using System.Data.SqlClient;
using System.Web;
class Dao {
public void Query() {
string id = Request.Form["id"];
string sql = "SELECT * FROM Users WHERE Id = " + id;
var cmd = new SqlCommand(sql);
cmd.ExecuteReader();
}
}
"#;
let findings = analyze(src, &sql_injection_spec());
assert!(
!findings.is_empty(),
"should detect Request.Form -> ExecuteReader: {findings:?}"
);
}
#[test]
fn sanitizer_htmlencode_blocks_xss() {
let src = r#"
using System.Web;
class Controller {
public void Handle() {
string raw = Request.QueryString["q"];
string safe = HttpUtility.HtmlEncode(raw);
Response.Write(safe);
}
}
"#;
let findings = analyze(src, &xss_spec());
assert!(
findings.is_empty(),
"HtmlEncode must block XSS finding: {findings:?}"
);
}
#[test]
fn xss_direct_response_write_no_sanitizer() {
let src = r#"
using System.Web;
class Controller {
public void Handle() {
string raw = Request.QueryString["q"];
Response.Write(raw);
}
}
"#;
let findings = analyze(src, &xss_spec());
assert!(!findings.is_empty(), "should detect XSS: {findings:?}");
}
#[test]
fn console_readline_to_process_start() {
let src = r#"
using System;
using System.Diagnostics;
class App {
static void Main() {
string cmd = Console.ReadLine();
Process.Start(cmd);
}
}
"#;
let findings = analyze(src, &command_injection_spec());
assert!(
!findings.is_empty(),
"should detect Console.ReadLine -> Process.Start: {findings:?}"
);
}
#[test]
fn clean_literal_no_finding() {
let src = r#"
using System.Diagnostics;
class App {
static void Main() {
string _ = Console.ReadLine();
Process.Start("notepad.exe");
}
}
"#;
let findings = analyze(src, &command_injection_spec());
assert!(
findings.is_empty(),
"literal argument must not trigger taint: {findings:?}"
);
}
#[test]
fn int_parse_sanitizes_numeric_sql_injection() {
let src = r#"
using System.Data.SqlClient;
using System.Web;
class Dao {
void Query() {
string rawId = Request.QueryString["id"];
int safeId = int.Parse(rawId);
string sql = "SELECT * FROM Users WHERE Id = " + safeId;
var cmd = new SqlCommand(sql);
cmd.ExecuteReader();
}
}
"#;
let findings = analyze(src, &sql_injection_spec());
assert!(
findings.is_empty(),
"int.Parse should sanitize SQL injection: {findings:?}"
);
}
#[test]
fn open_redirect_via_request_params() {
let src = r#"
using System.Web;
class Controller {
void Redirect() {
string url = Request.Params["returnUrl"];
Response.Redirect(url);
}
}
"#;
let findings = analyze(src, &open_redirect_spec());
assert!(
!findings.is_empty(),
"should detect open redirect: {findings:?}"
);
}
}