#![allow(clippy::needless_borrow)]
use std::collections::HashSet;
use crate::cfg::Cfg;
use crate::labels::{Cap, DataLabel};
use crate::ssa::ir::{SsaBody, SsaValue};
use crate::taint::Finding;
use super::state::SymbolicState;
use super::value::SymbolicValue;
pub fn extract_witness(
state: &SymbolicState,
finding: &Finding,
ssa: &SsaBody,
cfg: &Cfg,
) -> Option<String> {
let ssa_val = ssa.cfg_node_map.get(&finding.sink)?;
let sym = state.get(*ssa_val);
if matches!(sym, SymbolicValue::Unknown) {
return None;
}
let sym = unwrap_sink_call_arg(&sym, state);
let cap = sink_cap(finding, cfg);
let source_var = finding
.flow_steps
.iter()
.find(|s| matches!(s.op_kind, crate::evidence::FlowStepKind::Source))
.and_then(|s| s.var_name.as_deref())
.unwrap_or("input");
let sink_callee = if finding.sink.index() < cfg.node_count() {
cfg[finding.sink].call.callee.as_deref().unwrap_or("sink")
} else {
"sink"
};
let tainted = collect_tainted_symbols(&sym, state);
let field_paths: Vec<String> = state
.heap()
.field_accesses()
.iter()
.filter(|a| tainted.contains(&a.ssa_value))
.map(|a| format!("{}.{}", a.object_name, a.field_name))
.collect();
let field_suffix = if field_paths.is_empty() {
String::new()
} else {
format!(" via {}", field_paths.join(", "))
};
if tainted.is_empty() {
let concrete = evaluate_concrete(&sym);
Some(format!(
"input '{}' flows to {}(\"{}\")",
source_var, sink_callee, concrete
))
} else if is_string_renderable(&sym) {
let payload = witness_payload(cap);
let substituted = substitute_tainted(&sym, &tainted, payload);
let concrete = evaluate_concrete(&substituted);
let mismatch_suffix = detect_transform_mismatch(&sym, cap)
.map(|note| format!(" {}", note))
.unwrap_or_default();
Some(format!(
"input '{}' = \"{}\" flows to {}(\"{}\"){}{}",
source_var, payload, sink_callee, concrete, field_suffix, mismatch_suffix
))
} else {
let mismatch_suffix = detect_transform_mismatch(&sym, cap)
.map(|note| format!(" {}", note))
.unwrap_or_default();
Some(format!(
"tainted input '{}' reaches {}() unsanitized{}{}",
source_var, sink_callee, field_suffix, mismatch_suffix
))
}
}
fn unwrap_sink_call_arg<'a>(expr: &'a SymbolicValue, state: &SymbolicState) -> &'a SymbolicValue {
if let SymbolicValue::Call(_, args) = expr {
let best = args
.iter()
.filter(|a| !collect_tainted_symbols(a, state).is_empty())
.max_by_key(|a| arg_richness(a));
if let Some(arg) = best {
return arg;
}
}
expr
}
fn arg_richness(expr: &SymbolicValue) -> u32 {
match expr {
SymbolicValue::Encode(_, _) | SymbolicValue::Decode(_, _) => 100,
SymbolicValue::Concat(_, _) => 90,
SymbolicValue::BinOp(super::value::Op::Add, l, r)
if is_string_renderable(l) || is_string_renderable(r) =>
{
85
}
SymbolicValue::Replace(_, _, _)
| SymbolicValue::Substr(_, _, _)
| SymbolicValue::Trim(_)
| SymbolicValue::ToLower(_)
| SymbolicValue::ToUpper(_) => 80,
SymbolicValue::Symbol(_) => 50,
SymbolicValue::ConcreteStr(_) => 40,
SymbolicValue::Call(_, inner) => {
if inner.len() == 1 {
arg_richness(&inner[0]).saturating_sub(5)
} else {
20
}
}
SymbolicValue::Phi(_) => 15,
SymbolicValue::BinOp(_, _, _) => 10,
SymbolicValue::StrLen(_) | SymbolicValue::Concrete(_) | SymbolicValue::Unknown => 0,
}
}
fn sink_cap(finding: &Finding, cfg: &Cfg) -> Cap {
if finding.sink.index() >= cfg.node_count() {
return Cap::empty();
}
let info = &cfg[finding.sink];
let mut caps = Cap::empty();
for lbl in &info.taint.labels {
if let DataLabel::Sink(bits) = *lbl {
caps |= bits;
}
}
caps
}
fn witness_payload(cap: Cap) -> &'static str {
if cap.intersects(Cap::DATA_EXFIL) {
"<SESSION_TOKEN>"
} else if cap.intersects(Cap::CODE_EXEC) {
"require('child_process').execSync('id')"
} else if cap.intersects(Cap::HTML_ESCAPE) {
"<script>alert('xss')</script>"
} else if cap.intersects(Cap::SQL_QUERY) {
"' OR 1=1 --"
} else if cap.intersects(Cap::SHELL_ESCAPE) {
"$(id)"
} else if cap.intersects(Cap::FILE_IO) {
"../../etc/passwd"
} else if cap.intersects(Cap::SSRF) {
"http://169.254.169.254/metadata"
} else if cap.intersects(Cap::DESERIALIZE) {
"malicious_serialized_object"
} else {
"TAINTED"
}
}
fn is_string_renderable(expr: &SymbolicValue) -> bool {
match expr {
SymbolicValue::ConcreteStr(_) => true,
SymbolicValue::Symbol(_) => true,
SymbolicValue::Concat(l, r) => is_string_renderable(l) && is_string_renderable(r),
SymbolicValue::Trim(s)
| SymbolicValue::ToLower(s)
| SymbolicValue::ToUpper(s)
| SymbolicValue::Replace(s, _, _) => is_string_renderable(s),
SymbolicValue::Substr(s, _, _) => is_string_renderable(s),
SymbolicValue::Encode(_, s) | SymbolicValue::Decode(_, s) => is_string_renderable(s),
SymbolicValue::StrLen(_) => false,
SymbolicValue::BinOp(super::value::Op::Add, l, r) => {
is_string_renderable(l) && is_string_renderable(r)
}
SymbolicValue::Call(_, args) if args.len() == 1 => is_string_renderable(&args[0]),
SymbolicValue::Concrete(_)
| SymbolicValue::BinOp(_, _, _)
| SymbolicValue::Call(_, _)
| SymbolicValue::Phi(_)
| SymbolicValue::Unknown => false,
}
}
fn collect_tainted_symbols(expr: &SymbolicValue, state: &SymbolicState) -> HashSet<SsaValue> {
let mut tainted = HashSet::new();
collect_tainted_inner(expr, state, &mut tainted);
tainted
}
fn collect_tainted_inner(expr: &SymbolicValue, state: &SymbolicState, out: &mut HashSet<SsaValue>) {
match expr {
SymbolicValue::Symbol(v) => {
if state.is_tainted(*v) {
out.insert(*v);
}
}
SymbolicValue::BinOp(_, l, r) | SymbolicValue::Concat(l, r) => {
collect_tainted_inner(l, state, out);
collect_tainted_inner(r, state, out);
}
SymbolicValue::Call(_, args) => {
for arg in args {
collect_tainted_inner(arg, state, out);
}
}
SymbolicValue::Phi(ops) => {
for (_, v) in ops {
collect_tainted_inner(v, state, out);
}
}
SymbolicValue::ToLower(s)
| SymbolicValue::ToUpper(s)
| SymbolicValue::Trim(s)
| SymbolicValue::StrLen(s)
| SymbolicValue::Replace(s, _, _)
| SymbolicValue::Encode(_, s)
| SymbolicValue::Decode(_, s) => {
collect_tainted_inner(s, state, out);
}
SymbolicValue::Substr(s, start, end) => {
collect_tainted_inner(s, state, out);
collect_tainted_inner(start, state, out);
if let Some(e) = end {
collect_tainted_inner(e, state, out);
}
}
SymbolicValue::Concrete(_) | SymbolicValue::ConcreteStr(_) | SymbolicValue::Unknown => {}
}
}
fn substitute_tainted(
expr: &SymbolicValue,
tainted: &HashSet<SsaValue>,
payload: &str,
) -> SymbolicValue {
match expr {
SymbolicValue::Symbol(v) if tainted.contains(v) => {
SymbolicValue::ConcreteStr(payload.to_owned())
}
SymbolicValue::Concat(l, r) => {
let new_l = substitute_tainted(l, tainted, payload);
let new_r = substitute_tainted(r, tainted, payload);
if let (SymbolicValue::ConcreteStr(a), SymbolicValue::ConcreteStr(b)) = (&new_l, &new_r)
{
SymbolicValue::ConcreteStr(format!("{}{}", a, b))
} else {
SymbolicValue::Concat(Box::new(new_l), Box::new(new_r))
}
}
SymbolicValue::BinOp(op, l, r) => {
let new_l = substitute_tainted(l, tainted, payload);
let new_r = substitute_tainted(r, tainted, payload);
SymbolicValue::BinOp(*op, Box::new(new_l), Box::new(new_r))
}
SymbolicValue::Call(name, args) => {
let new_args: Vec<_> = args
.iter()
.map(|a| substitute_tainted(a, tainted, payload))
.collect();
SymbolicValue::Call(name.clone(), new_args)
}
SymbolicValue::Phi(ops) => {
let new_ops: Vec<_> = ops
.iter()
.map(|(bid, v)| (*bid, substitute_tainted(v, tainted, payload)))
.collect();
SymbolicValue::Phi(new_ops)
}
SymbolicValue::Trim(s) => {
SymbolicValue::Trim(Box::new(substitute_tainted(s, tainted, payload)))
}
SymbolicValue::ToLower(s) => {
SymbolicValue::ToLower(Box::new(substitute_tainted(s, tainted, payload)))
}
SymbolicValue::ToUpper(s) => {
SymbolicValue::ToUpper(Box::new(substitute_tainted(s, tainted, payload)))
}
SymbolicValue::StrLen(s) => {
SymbolicValue::StrLen(Box::new(substitute_tainted(s, tainted, payload)))
}
SymbolicValue::Replace(s, pat, rep) => SymbolicValue::Replace(
Box::new(substitute_tainted(s, tainted, payload)),
pat.clone(),
rep.clone(),
),
SymbolicValue::Substr(s, start, end) => SymbolicValue::Substr(
Box::new(substitute_tainted(s, tainted, payload)),
Box::new(substitute_tainted(start, tainted, payload)),
end.as_ref()
.map(|e| Box::new(substitute_tainted(e, tainted, payload))),
),
SymbolicValue::Encode(kind, s) => {
SymbolicValue::Encode(*kind, Box::new(substitute_tainted(s, tainted, payload)))
}
SymbolicValue::Decode(kind, s) => {
SymbolicValue::Decode(*kind, Box::new(substitute_tainted(s, tainted, payload)))
}
other => other.clone(),
}
}
fn evaluate_concrete(expr: &SymbolicValue) -> String {
match expr {
SymbolicValue::ConcreteStr(s) => s.clone(),
SymbolicValue::Concrete(n) => n.to_string(),
SymbolicValue::Concat(l, r) => {
let left = evaluate_concrete(l);
let right = evaluate_concrete(r);
format!("{}{}", left, right)
}
SymbolicValue::BinOp(super::value::Op::Add, l, r) if is_string_renderable(expr) => {
let left = evaluate_concrete(l);
let right = evaluate_concrete(r);
format!("{}{}", left, right)
}
SymbolicValue::Trim(s) => evaluate_concrete(s).trim().to_owned(),
SymbolicValue::ToLower(s) => evaluate_concrete(s).to_lowercase(),
SymbolicValue::ToUpper(s) => evaluate_concrete(s).to_uppercase(),
SymbolicValue::Replace(s, pat, rep) => {
evaluate_concrete(s).replace(pat.as_str(), rep.as_str())
}
SymbolicValue::Substr(s, start, end) => {
let inner = evaluate_concrete(s);
match (
start.as_concrete_int(),
end.as_ref().and_then(|e| e.as_concrete_int()),
) {
(Some(i), Some(j)) => {
let i = i.max(0) as usize;
let j = j.max(0) as usize;
inner.get(i..j.min(inner.len())).unwrap_or("").to_owned()
}
(Some(i), None) if end.is_none() => {
let i = i.max(0) as usize;
inner.get(i..).unwrap_or("").to_owned()
}
_ => format!("{}", expr),
}
}
SymbolicValue::StrLen(s) => {
if let SymbolicValue::ConcreteStr(cs) = s.as_ref() {
cs.len().to_string()
} else {
format!("{}", expr)
}
}
SymbolicValue::Encode(kind, s) => {
let inner = evaluate_concrete(s);
super::strings::encode_concrete_for_witness(*kind, &inner)
.unwrap_or_else(|| format!("{}", expr))
}
SymbolicValue::Decode(kind, s) => {
let inner = evaluate_concrete(s);
super::strings::decode_concrete_for_witness(*kind, &inner)
.unwrap_or_else(|| format!("{}", expr))
}
SymbolicValue::Call(_, args) if args.len() == 1 => evaluate_concrete(&args[0]),
other => format!("{}", other),
}
}
fn detect_transform_mismatch(expr: &SymbolicValue, sink_cap: Cap) -> Option<String> {
if sink_cap.is_empty() {
return None;
}
match expr {
SymbolicValue::Encode(kind, inner) => {
let neutralizes = kind.verified_cap();
if !neutralizes.is_empty() && !sink_cap.intersects(neutralizes) {
Some(format!(
"[transform note: {} does not match sink neutralization class ({})]",
kind.display_name(),
cap_description(sink_cap),
))
} else {
detect_transform_mismatch(inner, sink_cap)
}
}
SymbolicValue::Concat(l, r) => detect_transform_mismatch(l, sink_cap)
.or_else(|| detect_transform_mismatch(r, sink_cap)),
SymbolicValue::Trim(s)
| SymbolicValue::ToLower(s)
| SymbolicValue::ToUpper(s)
| SymbolicValue::Replace(s, _, _)
| SymbolicValue::Decode(_, s) => detect_transform_mismatch(s, sink_cap),
SymbolicValue::Substr(s, _, _) => detect_transform_mismatch(s, sink_cap),
_ => None,
}
}
fn cap_description(cap: Cap) -> &'static str {
if cap.intersects(Cap::SQL_QUERY) {
"sql_escape"
} else if cap.intersects(Cap::HTML_ESCAPE) {
"html_escape"
} else if cap.intersects(Cap::SHELL_ESCAPE) {
"shell_escape"
} else if cap.intersects(Cap::URL_ENCODE) {
"url_encode"
} else if cap.intersects(Cap::FILE_IO) {
"path_sanitization"
} else if cap.intersects(Cap::SSRF) {
"url_validation"
} else if cap.intersects(Cap::CODE_EXEC) {
"code_execution_sanitization"
} else {
"appropriate sanitization"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cfg::StmtKind;
use crate::ssa::ir::{BlockId, SsaValue};
use petgraph::graph::NodeIndex;
use smallvec::smallvec;
fn make_node_info(
labels: smallvec::SmallVec<[DataLabel; 2]>,
callee: Option<String>,
) -> crate::cfg::NodeInfo {
crate::cfg::NodeInfo {
kind: StmtKind::Seq,
call: crate::cfg::CallMeta {
callee,
..Default::default()
},
taint: crate::cfg::TaintMeta {
labels,
..Default::default()
},
..Default::default()
}
}
#[test]
fn test_sink_cap_extraction() {
let mut cfg = Cfg::new();
let n = cfg.add_node(make_node_info(
smallvec![DataLabel::Sink(Cap::SQL_QUERY)],
None,
));
let finding = Finding {
body_id: crate::cfg::BodyId(0),
sink: n,
source: NodeIndex::new(0),
path: vec![],
source_kind: crate::labels::SourceKind::UserInput,
path_validated: false,
guard_kind: None,
hop_count: 0,
cap_specificity: 0,
uses_summary: false,
flow_steps: vec![],
symbolic: None,
source_span: None,
primary_location: None,
engine_notes: smallvec::SmallVec::new(),
path_hash: 0,
finding_id: String::new(),
alternative_finding_ids: smallvec::SmallVec::new(),
effective_sink_caps: crate::labels::Cap::empty(),
};
assert_eq!(sink_cap(&finding, &cfg), Cap::SQL_QUERY);
}
#[test]
fn test_sink_cap_multiple_labels() {
let mut cfg = Cfg::new();
let n = cfg.add_node(make_node_info(
smallvec![
DataLabel::Sink(Cap::SQL_QUERY),
DataLabel::Source(Cap::ENV_VAR),
DataLabel::Sink(Cap::FILE_IO),
],
None,
));
let finding = Finding {
body_id: crate::cfg::BodyId(0),
sink: n,
source: NodeIndex::new(0),
path: vec![],
source_kind: crate::labels::SourceKind::UserInput,
path_validated: false,
guard_kind: None,
hop_count: 0,
cap_specificity: 0,
uses_summary: false,
flow_steps: vec![],
symbolic: None,
source_span: None,
primary_location: None,
engine_notes: smallvec::SmallVec::new(),
path_hash: 0,
finding_id: String::new(),
alternative_finding_ids: smallvec::SmallVec::new(),
effective_sink_caps: crate::labels::Cap::empty(),
};
let cap = sink_cap(&finding, &cfg);
assert!(cap.contains(Cap::SQL_QUERY));
assert!(cap.contains(Cap::FILE_IO));
assert!(!cap.contains(Cap::ENV_VAR)); }
#[test]
fn test_witness_payload_per_cap() {
assert_eq!(
witness_payload(Cap::CODE_EXEC),
"require('child_process').execSync('id')"
);
assert_eq!(witness_payload(Cap::SQL_QUERY), "' OR 1=1 --");
assert_eq!(witness_payload(Cap::SHELL_ESCAPE), "$(id)");
assert_eq!(witness_payload(Cap::FILE_IO), "../../etc/passwd");
assert_eq!(
witness_payload(Cap::SSRF),
"http://169.254.169.254/metadata"
);
assert_eq!(
witness_payload(Cap::DESERIALIZE),
"malicious_serialized_object"
);
assert_eq!(witness_payload(Cap::DATA_EXFIL), "<SESSION_TOKEN>");
assert_eq!(witness_payload(Cap::CRYPTO), "TAINTED"); }
#[test]
fn test_witness_payload_data_exfil_wins_over_action_caps() {
let combined = Cap::DATA_EXFIL | Cap::SSRF;
assert_eq!(witness_payload(combined), "<SESSION_TOKEN>");
}
#[test]
fn test_witness_payload_code_exec_separate_from_xss() {
let code_exec = witness_payload(Cap::CODE_EXEC);
assert!(
code_exec.contains("child_process"),
"CODE_EXEC payload should be code-execution, got: {code_exec}"
);
assert!(
!code_exec.contains("script"),
"CODE_EXEC payload must not be an XSS payload"
);
let xss = witness_payload(Cap::HTML_ESCAPE);
assert!(
xss.contains("script"),
"HTML_ESCAPE payload should be XSS, got: {xss}"
);
}
#[test]
fn test_witness_payload_combined_caps_prefers_code_exec() {
let combined = Cap::CODE_EXEC | Cap::HTML_ESCAPE;
let payload = witness_payload(combined);
assert_eq!(
payload, "require('child_process').execSync('id')",
"CODE_EXEC should take priority over HTML_ESCAPE"
);
}
#[test]
fn test_witness_payload_unrelated_caps_unchanged() {
assert_eq!(witness_payload(Cap::SQL_QUERY), "' OR 1=1 --");
assert_eq!(witness_payload(Cap::SHELL_ESCAPE), "$(id)");
assert_eq!(witness_payload(Cap::FILE_IO), "../../etc/passwd");
assert_eq!(
witness_payload(Cap::SSRF),
"http://169.254.169.254/metadata"
);
assert_eq!(
witness_payload(Cap::DESERIALIZE),
"malicious_serialized_object"
);
let sql_file = Cap::SQL_QUERY | Cap::FILE_IO;
assert_eq!(
witness_payload(sql_file),
"' OR 1=1 --",
"SQL_QUERY should take priority over FILE_IO"
);
}
#[test]
fn test_is_string_renderable() {
assert!(is_string_renderable(&SymbolicValue::ConcreteStr(
"hello".into()
)));
assert!(is_string_renderable(&SymbolicValue::Symbol(SsaValue(0))));
assert!(is_string_renderable(&SymbolicValue::Concat(
Box::new(SymbolicValue::ConcreteStr("a".into())),
Box::new(SymbolicValue::Symbol(SsaValue(1))),
)));
assert!(!is_string_renderable(&SymbolicValue::Concrete(42)));
assert!(!is_string_renderable(&SymbolicValue::BinOp(
super::super::value::Op::Add,
Box::new(SymbolicValue::Symbol(SsaValue(0))),
Box::new(SymbolicValue::Concrete(5)),
)));
assert!(!is_string_renderable(&SymbolicValue::Call(
"foo".into(),
vec![],
)));
assert!(!is_string_renderable(&SymbolicValue::Unknown));
}
#[test]
fn test_substitute_tainted_concat() {
let expr = SymbolicValue::Concat(
Box::new(SymbolicValue::ConcreteStr(
"SELECT * FROM t WHERE id = ".into(),
)),
Box::new(SymbolicValue::Symbol(SsaValue(5))),
);
let mut tainted = HashSet::new();
tainted.insert(SsaValue(5));
let result = substitute_tainted(&expr, &tainted, "' OR 1=1 --");
assert_eq!(
evaluate_concrete(&result),
"SELECT * FROM t WHERE id = ' OR 1=1 --"
);
}
#[test]
fn test_extract_witness_sqli() {
use crate::taint::FlowStepRaw;
let mut state = SymbolicState::new();
let sink_val = SsaValue(10);
let tainted_val = SsaValue(5);
state.set(
sink_val,
SymbolicValue::Concat(
Box::new(SymbolicValue::ConcreteStr(
"SELECT * FROM t WHERE id = ".into(),
)),
Box::new(SymbolicValue::Symbol(tainted_val)),
),
);
state.mark_tainted(tainted_val);
let mut cfg = Cfg::new();
let sink_node = cfg.add_node(make_node_info(
smallvec![DataLabel::Sink(Cap::SQL_QUERY)],
Some("query".into()),
));
let source_node = cfg.add_node(make_node_info(smallvec![], None));
let ssa = SsaBody {
blocks: vec![],
entry: BlockId(0),
value_defs: vec![],
cfg_node_map: [(sink_node, sink_val)].into_iter().collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let finding = Finding {
body_id: crate::cfg::BodyId(0),
sink: sink_node,
source: source_node,
path: vec![source_node, sink_node],
source_kind: crate::labels::SourceKind::UserInput,
path_validated: false,
guard_kind: None,
hop_count: 1,
cap_specificity: 1,
uses_summary: false,
flow_steps: vec![
FlowStepRaw {
cfg_node: source_node,
var_name: Some("userInput".into()),
op_kind: crate::evidence::FlowStepKind::Source,
},
FlowStepRaw {
cfg_node: sink_node,
var_name: Some("userInput".into()),
op_kind: crate::evidence::FlowStepKind::Sink,
},
],
symbolic: None,
source_span: None,
primary_location: None,
engine_notes: smallvec::SmallVec::new(),
path_hash: 0,
finding_id: String::new(),
alternative_finding_ids: smallvec::SmallVec::new(),
effective_sink_caps: crate::labels::Cap::empty(),
};
let witness = extract_witness(&state, &finding, &ssa, &cfg);
assert!(witness.is_some());
let w = witness.unwrap();
assert!(w.contains("' OR 1=1 --"), "witness: {}", w);
assert!(w.contains("flows to"), "witness: {}", w);
assert!(w.contains("query"), "witness: {}", w);
}
#[test]
fn test_extract_witness_unknown_returns_none() {
let state = SymbolicState::new();
let sink_node = NodeIndex::new(10);
let ssa = SsaBody {
blocks: vec![],
entry: BlockId(0),
value_defs: vec![],
cfg_node_map: [(sink_node, SsaValue(5))].into_iter().collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let cfg = Cfg::new();
let finding = Finding {
body_id: crate::cfg::BodyId(0),
sink: sink_node,
source: NodeIndex::new(0),
path: vec![],
source_kind: crate::labels::SourceKind::UserInput,
path_validated: false,
guard_kind: None,
hop_count: 0,
cap_specificity: 0,
uses_summary: false,
flow_steps: vec![],
symbolic: None,
source_span: None,
primary_location: None,
engine_notes: smallvec::SmallVec::new(),
path_hash: 0,
finding_id: String::new(),
alternative_finding_ids: smallvec::SmallVec::new(),
effective_sink_caps: crate::labels::Cap::empty(),
};
assert!(extract_witness(&state, &finding, &ssa, &cfg).is_none());
}
#[test]
fn test_non_string_renderable_generic_witness() {
use crate::taint::FlowStepRaw;
let mut state = SymbolicState::new();
let sink_val = SsaValue(10);
let tainted_val = SsaValue(5);
state.set(
sink_val,
SymbolicValue::BinOp(
super::super::value::Op::Add,
Box::new(SymbolicValue::Symbol(tainted_val)),
Box::new(SymbolicValue::Concrete(5)),
),
);
state.mark_tainted(tainted_val);
let mut cfg = Cfg::new();
let sink_node = cfg.add_node(make_node_info(
smallvec![DataLabel::Sink(Cap::SQL_QUERY)],
Some("execute".into()),
));
let source_node = cfg.add_node(make_node_info(smallvec![], None));
let ssa = SsaBody {
blocks: vec![],
entry: BlockId(0),
value_defs: vec![],
cfg_node_map: [(sink_node, sink_val)].into_iter().collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let finding = Finding {
body_id: crate::cfg::BodyId(0),
sink: sink_node,
source: source_node,
path: vec![source_node, sink_node],
source_kind: crate::labels::SourceKind::UserInput,
path_validated: false,
guard_kind: None,
hop_count: 1,
cap_specificity: 1,
uses_summary: false,
flow_steps: vec![FlowStepRaw {
cfg_node: source_node,
var_name: Some("count".into()),
op_kind: crate::evidence::FlowStepKind::Source,
}],
symbolic: None,
source_span: None,
primary_location: None,
engine_notes: smallvec::SmallVec::new(),
path_hash: 0,
finding_id: String::new(),
alternative_finding_ids: smallvec::SmallVec::new(),
effective_sink_caps: crate::labels::Cap::empty(),
};
let witness = extract_witness(&state, &finding, &ssa, &cfg);
assert!(witness.is_some());
let w = witness.unwrap();
assert!(w.contains("reaches"), "witness: {}", w);
assert!(w.contains("unsanitized"), "witness: {}", w);
assert!(w.contains("execute"), "witness: {}", w);
assert!(!w.contains("' OR 1=1"), "witness: {}", w);
}
#[test]
fn test_no_tainted_symbols() {
use crate::taint::FlowStepRaw;
let mut state = SymbolicState::new();
let sink_val = SsaValue(10);
state.set(sink_val, SymbolicValue::ConcreteStr("SELECT 1".into()));
let mut cfg = Cfg::new();
let sink_node = cfg.add_node(make_node_info(
smallvec![DataLabel::Sink(Cap::SQL_QUERY)],
Some("query".into()),
));
let source_node = cfg.add_node(make_node_info(smallvec![], None));
let ssa = SsaBody {
blocks: vec![],
entry: BlockId(0),
value_defs: vec![],
cfg_node_map: [(sink_node, sink_val)].into_iter().collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let finding = Finding {
body_id: crate::cfg::BodyId(0),
sink: sink_node,
source: source_node,
path: vec![source_node, sink_node],
source_kind: crate::labels::SourceKind::UserInput,
path_validated: false,
guard_kind: None,
hop_count: 1,
cap_specificity: 1,
uses_summary: false,
flow_steps: vec![FlowStepRaw {
cfg_node: source_node,
var_name: Some("x".into()),
op_kind: crate::evidence::FlowStepKind::Source,
}],
symbolic: None,
source_span: None,
primary_location: None,
engine_notes: smallvec::SmallVec::new(),
path_hash: 0,
finding_id: String::new(),
alternative_finding_ids: smallvec::SmallVec::new(),
effective_sink_caps: crate::labels::Cap::empty(),
};
let witness = extract_witness(&state, &finding, &ssa, &cfg);
assert!(witness.is_some());
let w = witness.unwrap();
assert!(w.contains("flows to"), "witness: {}", w);
assert!(w.contains("SELECT 1"), "witness: {}", w);
}
#[test]
fn test_string_ops_are_string_renderable() {
assert!(is_string_renderable(&SymbolicValue::Trim(Box::new(
SymbolicValue::Symbol(SsaValue(0))
))));
assert!(is_string_renderable(&SymbolicValue::ToLower(Box::new(
SymbolicValue::Symbol(SsaValue(0))
))));
assert!(is_string_renderable(&SymbolicValue::ToUpper(Box::new(
SymbolicValue::Symbol(SsaValue(0))
))));
assert!(is_string_renderable(&SymbolicValue::Replace(
Box::new(SymbolicValue::Symbol(SsaValue(0))),
"<".into(),
"<".into(),
)));
assert!(is_string_renderable(&SymbolicValue::Substr(
Box::new(SymbolicValue::Symbol(SsaValue(0))),
Box::new(SymbolicValue::Concrete(0)),
Some(Box::new(SymbolicValue::Concrete(5))),
)));
assert!(!is_string_renderable(&SymbolicValue::StrLen(Box::new(
SymbolicValue::Symbol(SsaValue(0))
))));
}
#[test]
fn test_evaluate_concrete_string_ops() {
let v = SymbolicValue::Trim(Box::new(SymbolicValue::ConcreteStr(" hi ".into())));
assert_eq!(evaluate_concrete(&v), "hi");
let v = SymbolicValue::ToLower(Box::new(SymbolicValue::ConcreteStr("ABC".into())));
assert_eq!(evaluate_concrete(&v), "abc");
let v = SymbolicValue::Replace(
Box::new(SymbolicValue::ConcreteStr("a<b".into())),
"<".into(),
"<".into(),
);
assert_eq!(evaluate_concrete(&v), "a<b");
}
#[test]
fn test_substitute_tainted_through_string_ops() {
let tainted_val = SsaValue(5);
let mut tainted = HashSet::new();
tainted.insert(tainted_val);
let expr = SymbolicValue::Concat(
Box::new(SymbolicValue::ConcreteStr("prefix".into())),
Box::new(SymbolicValue::Trim(Box::new(SymbolicValue::Symbol(
tainted_val,
)))),
);
let result = substitute_tainted(&expr, &tainted, "PAYLOAD");
assert_eq!(evaluate_concrete(&result), "prefixPAYLOAD");
}
#[test]
fn test_collect_tainted_through_string_ops() {
let tainted_val = SsaValue(5);
let mut state = SymbolicState::new();
state.mark_tainted(tainted_val);
let expr = SymbolicValue::ToLower(Box::new(SymbolicValue::Symbol(tainted_val)));
let tainted = collect_tainted_symbols(&expr, &state);
assert!(tainted.contains(&tainted_val));
}
#[test]
fn test_encoding_is_string_renderable() {
use super::super::strings::TransformKind;
let v = SymbolicValue::Encode(
TransformKind::HtmlEscape,
Box::new(SymbolicValue::Symbol(SsaValue(0))),
);
assert!(is_string_renderable(&v));
let v = SymbolicValue::Decode(
TransformKind::UrlDecode,
Box::new(SymbolicValue::Symbol(SsaValue(1))),
);
assert!(is_string_renderable(&v));
}
#[test]
fn test_substitute_tainted_through_encode() {
use super::super::strings::TransformKind;
let tainted_val = SsaValue(10);
let mut tainted = HashSet::new();
tainted.insert(tainted_val);
let expr = SymbolicValue::Encode(
TransformKind::HtmlEscape,
Box::new(SymbolicValue::Symbol(tainted_val)),
);
let result = substitute_tainted(&expr, &tainted, "<script>");
match &result {
SymbolicValue::Encode(kind, inner) => {
assert_eq!(*kind, TransformKind::HtmlEscape);
assert_eq!(**inner, SymbolicValue::ConcreteStr("<script>".into()));
}
other => panic!("expected Encode, got {:?}", other),
}
}
#[test]
fn test_evaluate_concrete_encode() {
use super::super::strings::TransformKind;
let v = SymbolicValue::Encode(
TransformKind::HtmlEscape,
Box::new(SymbolicValue::ConcreteStr("<b>hi</b>".into())),
);
assert_eq!(evaluate_concrete(&v), "<b>hi</b>");
}
#[test]
fn test_evaluate_concrete_decode() {
use super::super::strings::TransformKind;
let v = SymbolicValue::Decode(
TransformKind::UrlDecode,
Box::new(SymbolicValue::ConcreteStr("hello%20world".into())),
);
assert_eq!(evaluate_concrete(&v), "hello world");
}
#[test]
fn test_collect_tainted_through_encode() {
use super::super::strings::TransformKind;
let tainted_val = SsaValue(20);
let mut state = SymbolicState::new();
state.mark_tainted(tainted_val);
let expr = SymbolicValue::Encode(
TransformKind::UrlEncode,
Box::new(SymbolicValue::Symbol(tainted_val)),
);
let tainted = collect_tainted_symbols(&expr, &state);
assert!(tainted.contains(&tainted_val));
}
#[test]
fn test_detect_mismatch_url_at_sql_sink() {
use super::super::strings::TransformKind;
let expr = SymbolicValue::Encode(
TransformKind::UrlEncode,
Box::new(SymbolicValue::Symbol(SsaValue(0))),
);
let result = detect_transform_mismatch(&expr, Cap::SQL_QUERY);
assert!(result.is_some());
let note = result.unwrap();
assert!(note.contains("urlEncode"));
assert!(note.contains("does not match sink neutralization class"));
assert!(note.contains("sql_escape"));
}
#[test]
fn test_no_mismatch_when_encoding_matches_sink() {
use super::super::strings::TransformKind;
let expr = SymbolicValue::Encode(
TransformKind::HtmlEscape,
Box::new(SymbolicValue::Symbol(SsaValue(0))),
);
assert!(detect_transform_mismatch(&expr, Cap::HTML_ESCAPE).is_none());
}
#[test]
fn test_no_mismatch_for_representation_transform() {
use super::super::strings::TransformKind;
let expr = SymbolicValue::Encode(
TransformKind::Base64Encode,
Box::new(SymbolicValue::Symbol(SsaValue(0))),
);
assert!(detect_transform_mismatch(&expr, Cap::SQL_QUERY).is_none());
}
#[test]
fn test_no_mismatch_for_sql_escape() {
use super::super::strings::TransformKind;
let expr = SymbolicValue::Encode(
TransformKind::SqlEscape,
Box::new(SymbolicValue::Symbol(SsaValue(0))),
);
assert!(detect_transform_mismatch(&expr, Cap::SQL_QUERY).is_none());
}
#[test]
fn test_mismatch_through_concat() {
use super::super::strings::TransformKind;
let encoded = SymbolicValue::Encode(
TransformKind::ShellEscape,
Box::new(SymbolicValue::Symbol(SsaValue(0))),
);
let expr = SymbolicValue::Concat(
Box::new(SymbolicValue::ConcreteStr("prefix".into())),
Box::new(encoded),
);
let result = detect_transform_mismatch(&expr, Cap::SQL_QUERY);
assert!(result.is_some());
assert!(result.unwrap().contains("shellEscape"));
}
#[test]
fn test_no_mismatch_empty_sink_cap() {
use super::super::strings::TransformKind;
let expr = SymbolicValue::Encode(
TransformKind::UrlEncode,
Box::new(SymbolicValue::Symbol(SsaValue(0))),
);
assert!(detect_transform_mismatch(&expr, Cap::empty()).is_none());
}
}