use std::collections::{HashMap, HashSet};
use std::fmt;
use std::hash::{Hash, Hasher};
#[derive(Clone, Eq)]
pub struct FieldPath {
pub base: String,
pub fields: Vec<String>,
}
impl FieldPath {
pub fn new(base: impl Into<String>) -> Self {
Self {
base: base.into(),
fields: Vec::new(),
}
}
pub fn with_field(base: impl Into<String>, field: impl Into<String>) -> Self {
Self {
base: base.into(),
fields: vec![field.into()],
}
}
pub fn from_dotted(path: &str) -> Self {
let parts: Vec<&str> = path.split('.').collect();
if parts.is_empty() {
return Self::new("");
}
Self {
base: parts[0].to_string(),
fields: parts[1..].iter().map(|s| s.to_string()).collect(),
}
}
pub fn append(&self, field: impl Into<String>) -> Self {
let mut new_fields = self.fields.clone();
new_fields.push(field.into());
Self {
base: self.base.clone(),
fields: new_fields,
}
}
pub fn parent(&self) -> Option<Self> {
if self.fields.is_empty() {
None
} else {
Some(Self {
base: self.base.clone(),
fields: self.fields[..self.fields.len() - 1].to_vec(),
})
}
}
pub fn last_field(&self) -> Option<&str> {
self.fields.last().map(|s| s.as_str())
}
pub fn is_prefix_of(&self, other: &FieldPath) -> bool {
if self.base != other.base {
return false;
}
if self.fields.len() > other.fields.len() {
return false;
}
self.fields
.iter()
.zip(other.fields.iter())
.all(|(a, b)| a == b)
}
pub fn starts_with(&self, other: &FieldPath) -> bool {
other.is_prefix_of(self)
}
pub fn to_dotted(&self) -> String {
if self.fields.is_empty() {
self.base.clone()
} else {
format!("{}.{}", self.base, self.fields.join("."))
}
}
pub fn depth(&self) -> usize {
self.fields.len()
}
pub fn is_base(&self) -> bool {
self.fields.is_empty()
}
}
impl PartialEq for FieldPath {
fn eq(&self, other: &Self) -> bool {
self.base == other.base && self.fields == other.fields
}
}
impl Hash for FieldPath {
fn hash<H: Hasher>(&self, state: &mut H) {
self.base.hash(state);
self.fields.hash(state);
}
}
impl fmt::Debug for FieldPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "FieldPath({})", self.to_dotted())
}
}
impl fmt::Display for FieldPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_dotted())
}
}
impl From<&str> for FieldPath {
fn from(s: &str) -> Self {
FieldPath::from_dotted(s)
}
}
impl From<String> for FieldPath {
fn from(s: String) -> Self {
FieldPath::from_dotted(&s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldTaintStatus {
Clean,
Tainted,
Sanitized,
Unknown,
}
impl FieldTaintStatus {
pub fn is_tainted(&self) -> bool {
matches!(self, FieldTaintStatus::Tainted)
}
pub fn is_clean(&self) -> bool {
matches!(self, FieldTaintStatus::Clean | FieldTaintStatus::Sanitized)
}
}
#[derive(Debug, Clone)]
pub struct FieldTaintInfo {
pub status: FieldTaintStatus,
pub taint_line: Option<usize>,
pub source: Option<String>,
pub sanitized_line: Option<usize>,
}
impl Default for FieldTaintInfo {
fn default() -> Self {
Self {
status: FieldTaintStatus::Unknown,
taint_line: None,
source: None,
sanitized_line: None,
}
}
}
impl FieldTaintInfo {
pub fn tainted(line: Option<usize>, source: Option<String>) -> Self {
Self {
status: FieldTaintStatus::Tainted,
taint_line: line,
source,
sanitized_line: None,
}
}
pub fn clean() -> Self {
Self {
status: FieldTaintStatus::Clean,
taint_line: None,
source: None,
sanitized_line: None,
}
}
pub fn sanitized(line: usize) -> Self {
Self {
status: FieldTaintStatus::Sanitized,
taint_line: None,
source: None,
sanitized_line: Some(line),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct FieldTaintMap {
taint_map: HashMap<FieldPath, FieldTaintInfo>,
tainted_bases: HashSet<String>,
}
impl FieldTaintMap {
pub fn new() -> Self {
Self::default()
}
pub fn mark_tainted(&mut self, path: FieldPath, line: Option<usize>, source: Option<String>) {
self.tainted_bases.insert(path.base.clone());
self.taint_map
.insert(path, FieldTaintInfo::tainted(line, source));
}
pub fn mark_tainted_dotted(&mut self, path: &str, line: Option<usize>, source: Option<String>) {
self.mark_tainted(FieldPath::from_dotted(path), line, source);
}
pub fn mark_clean(&mut self, path: &FieldPath) {
self.taint_map.insert(path.clone(), FieldTaintInfo::clean());
if !self.has_any_tainted_field(&path.base) {
self.tainted_bases.remove(&path.base);
}
}
pub fn mark_sanitized(&mut self, path: &FieldPath, line: usize) {
self.taint_map
.insert(path.clone(), FieldTaintInfo::sanitized(line));
if !self.has_any_tainted_field(&path.base) {
self.tainted_bases.remove(&path.base);
}
}
pub fn is_tainted(&self, path: &FieldPath) -> bool {
if let Some(info) = self.taint_map.get(path) {
if info.status.is_tainted() {
return true;
}
if info.status.is_clean() {
return false;
}
}
let mut current = path.clone();
while let Some(parent) = current.parent() {
if let Some(info) = self.taint_map.get(&parent)
&& info.status.is_tainted()
{
return true;
}
current = parent;
}
if path.depth() > 0 {
let base_path = FieldPath::new(&path.base);
if let Some(info) = self.taint_map.get(&base_path) {
return info.status.is_tainted();
}
}
false
}
pub fn is_tainted_dotted(&self, path: &str) -> bool {
self.is_tainted(&FieldPath::from_dotted(path))
}
pub fn has_any_tainted_field(&self, base: &str) -> bool {
if !self.tainted_bases.contains(base) {
return false;
}
self.taint_map
.iter()
.any(|(path, info)| path.base == base && info.status.is_tainted())
}
pub fn tainted_fields_of(&self, base: &str) -> Vec<&FieldPath> {
self.taint_map
.iter()
.filter(|(path, info)| path.base == base && info.status.is_tainted())
.map(|(path, _)| path)
.collect()
}
pub fn all_tainted(&self) -> Vec<&FieldPath> {
self.taint_map
.iter()
.filter(|(_, info)| info.status.is_tainted())
.map(|(path, _)| path)
.collect()
}
pub fn get_info(&self, path: &FieldPath) -> Option<&FieldTaintInfo> {
self.taint_map.get(path)
}
pub fn handle_property_assignment(
&mut self,
target_path: FieldPath,
value_tainted: bool,
line: Option<usize>,
source: Option<String>,
) {
if value_tainted {
self.mark_tainted(target_path, line, source);
} else {
self.mark_clean(&target_path);
}
}
pub fn handle_property_read(&self, source_path: &FieldPath) -> bool {
self.is_tainted(source_path)
}
pub fn handle_destructuring(
&self,
source: &FieldPath,
field_names: &[&str],
) -> HashMap<String, bool> {
let mut result = HashMap::new();
for field in field_names {
let field_path = source.append(*field);
result.insert(field.to_string(), self.is_tainted(&field_path));
}
result
}
pub fn handle_spread_with_override(
&self,
source: &FieldPath,
overrides: &HashMap<String, bool>, result_base: &str,
line: Option<usize>,
) -> FieldTaintMap {
let mut result = FieldTaintMap::new();
for (path, info) in &self.taint_map {
if path.base == source.base {
if let Some(field) = path.fields.first()
&& overrides.contains_key(field)
{
continue; }
let new_path = FieldPath {
base: result_base.to_string(),
fields: path.fields.clone(),
};
result.taint_map.insert(new_path, info.clone());
if info.status.is_tainted() {
result.tainted_bases.insert(result_base.to_string());
}
}
}
for (field, is_tainted) in overrides {
let path = FieldPath::with_field(result_base, field);
if *is_tainted {
result.mark_tainted(path, line, None);
} else {
result.mark_clean(&path);
}
}
result
}
pub fn handle_array_destructuring(&self, source: &FieldPath, count: usize) -> Vec<bool> {
let mut result = Vec::with_capacity(count);
for i in 0..count {
let index_path = source.append(i.to_string());
result.push(self.is_tainted(&index_path));
}
result
}
pub fn handle_computed_access(&self, source: &FieldPath, key: Option<&str>) -> bool {
match key {
Some(field) => {
let field_path = source.append(field);
self.is_tainted(&field_path)
}
None => {
self.has_any_tainted_field(&source.base)
}
}
}
pub fn merge(&mut self, other: &FieldTaintMap) {
for (path, info) in &other.taint_map {
match self.taint_map.get(path) {
Some(existing) => {
if info.status.is_tainted() && !existing.status.is_tainted() {
self.taint_map.insert(path.clone(), info.clone());
self.tainted_bases.insert(path.base.clone());
}
}
None => {
self.taint_map.insert(path.clone(), info.clone());
if info.status.is_tainted() {
self.tainted_bases.insert(path.base.clone());
}
}
}
}
}
pub fn iter(&self) -> impl Iterator<Item = (&FieldPath, &FieldTaintInfo)> {
self.taint_map.iter()
}
pub fn len(&self) -> usize {
self.taint_map.len()
}
pub fn is_empty(&self) -> bool {
self.taint_map.is_empty()
}
pub fn clear(&mut self) {
self.taint_map.clear();
self.tainted_bases.clear();
}
}
#[derive(Debug, Clone, Default)]
pub struct FieldSensitiveTaintResult {
pub field_taint: FieldTaintMap,
pub fully_tainted_vars: HashSet<String>,
pub field_flows: Vec<FieldTaintFlow>,
}
#[derive(Debug, Clone)]
pub struct FieldTaintFlow {
pub source: FieldPath,
pub sink: FieldPath,
pub source_line: usize,
pub sink_line: usize,
pub path: Vec<FieldPath>,
}
impl FieldSensitiveTaintResult {
pub fn new() -> Self {
Self::default()
}
pub fn is_field_tainted(&self, path: &FieldPath) -> bool {
if self.fully_tainted_vars.contains(&path.base) {
return true;
}
self.field_taint.is_tainted(path)
}
pub fn is_tainted(&self, var_name: &str) -> bool {
if self.fully_tainted_vars.contains(var_name) {
return true;
}
let path = FieldPath::from_dotted(var_name);
self.is_field_tainted(&path)
}
pub fn mark_fully_tainted(&mut self, var_name: impl Into<String>) {
self.fully_tainted_vars.insert(var_name.into());
}
pub fn add_flow(&mut self, flow: FieldTaintFlow) {
self.field_flows.push(flow);
}
pub fn flows(&self) -> &[FieldTaintFlow] {
&self.field_flows
}
pub fn all_tainted_paths(&self) -> Vec<FieldPath> {
let mut paths: Vec<_> = self
.field_taint
.all_tainted()
.into_iter()
.cloned()
.collect();
for var in &self.fully_tainted_vars {
paths.push(FieldPath::new(var));
}
paths
}
}
pub struct FieldSensitiveAnalyzer {
field_taint: FieldTaintMap,
fully_tainted: HashSet<String>,
flows: Vec<FieldTaintFlow>,
}
impl FieldSensitiveAnalyzer {
pub fn new() -> Self {
Self {
field_taint: FieldTaintMap::new(),
fully_tainted: HashSet::new(),
flows: Vec::new(),
}
}
pub fn process_property_assignment(
&mut self,
target: &str,
field: &str,
value_source: Option<&FieldPath>,
value_is_tainted: bool,
line: usize,
) {
let target_path = FieldPath::with_field(target, field);
if value_is_tainted {
let source = value_source.map(|p| p.to_dotted());
self.field_taint
.mark_tainted(target_path.clone(), Some(line), source);
} else {
self.field_taint.mark_clean(&target_path);
}
}
pub fn process_property_read(&self, source: &str, field: &str) -> bool {
let source_path = FieldPath::with_field(source, field);
self.field_taint.is_tainted(&source_path)
}
pub fn process_destructuring(
&mut self,
source: &str,
bindings: &[(&str, &str)], line: usize,
) {
let source_path = FieldPath::new(source);
for (field, var_name) in bindings {
let field_path = source_path.append(*field);
let is_tainted = self.field_taint.is_tainted(&field_path);
if is_tainted {
self.field_taint.mark_tainted(
FieldPath::new(*var_name),
Some(line),
Some(field_path.to_dotted()),
);
}
}
}
pub fn process_spread_with_override(
&mut self,
source: &str,
overrides: Vec<(&str, bool)>, result_var: &str,
line: usize,
) {
let source_path = FieldPath::new(source);
let override_map: HashMap<String, bool> = overrides
.into_iter()
.map(|(f, t)| (f.to_string(), t))
.collect();
let result_taint = self.field_taint.handle_spread_with_override(
&source_path,
&override_map,
result_var,
Some(line),
);
self.field_taint.merge(&result_taint);
}
pub fn mark_fully_tainted(&mut self, var_name: &str, line: usize, source: Option<String>) {
self.fully_tainted.insert(var_name.to_string());
self.field_taint
.mark_tainted(FieldPath::new(var_name), Some(line), source);
}
pub fn is_tainted(&self, path: &str) -> bool {
let field_path = FieldPath::from_dotted(path);
if self.fully_tainted.contains(&field_path.base) {
return true;
}
self.field_taint.is_tainted(&field_path)
}
pub fn record_flow(
&mut self,
source: FieldPath,
sink: FieldPath,
source_line: usize,
sink_line: usize,
) {
self.flows.push(FieldTaintFlow {
source,
sink,
source_line,
sink_line,
path: Vec::new(),
});
}
pub fn build(self) -> FieldSensitiveTaintResult {
FieldSensitiveTaintResult {
field_taint: self.field_taint,
fully_tainted_vars: self.fully_tainted,
field_flows: self.flows,
}
}
}
impl Default for FieldSensitiveAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_field_path_creation() {
let path = FieldPath::new("obj");
assert_eq!(path.base, "obj");
assert!(path.fields.is_empty());
assert_eq!(path.to_dotted(), "obj");
let path2 = FieldPath::with_field("obj", "field");
assert_eq!(path2.to_dotted(), "obj.field");
let path3 = FieldPath::from_dotted("obj.field.subfield");
assert_eq!(path3.base, "obj");
assert_eq!(path3.fields, vec!["field", "subfield"]);
}
#[test]
fn test_field_path_append() {
let path = FieldPath::new("obj");
let path2 = path.append("field");
assert_eq!(path2.to_dotted(), "obj.field");
let path3 = path2.append("subfield");
assert_eq!(path3.to_dotted(), "obj.field.subfield");
}
#[test]
fn test_field_path_parent() {
let path = FieldPath::from_dotted("obj.field.subfield");
let parent = path.parent().unwrap();
assert_eq!(parent.to_dotted(), "obj.field");
let grandparent = parent.parent().unwrap();
assert_eq!(grandparent.to_dotted(), "obj");
assert!(grandparent.parent().is_none());
}
#[test]
fn test_field_path_prefix() {
let path1 = FieldPath::from_dotted("obj.field");
let path2 = FieldPath::from_dotted("obj.field.subfield");
let path3 = FieldPath::from_dotted("obj.other");
assert!(path1.is_prefix_of(&path2));
assert!(!path2.is_prefix_of(&path1));
assert!(!path1.is_prefix_of(&path3));
}
#[test]
fn test_field_taint_map_basic() {
let mut map = FieldTaintMap::new();
map.mark_tainted_dotted("obj.field", Some(10), Some("userInput".to_string()));
assert!(map.is_tainted_dotted("obj.field"));
assert!(!map.is_tainted_dotted("obj.other"));
assert!(!map.is_tainted_dotted("obj")); }
#[test]
fn test_field_taint_propagation_down() {
let mut map = FieldTaintMap::new();
map.mark_tainted(FieldPath::new("obj"), Some(10), None);
assert!(map.is_tainted_dotted("obj.field"));
assert!(map.is_tainted_dotted("obj.field.subfield"));
}
#[test]
fn test_property_assignment() {
let mut map = FieldTaintMap::new();
map.handle_property_assignment(
FieldPath::with_field("obj", "field"),
true,
Some(10),
Some("userInput".to_string()),
);
assert!(map.is_tainted_dotted("obj.field"));
assert!(!map.is_tainted_dotted("obj.other"));
map.handle_property_assignment(
FieldPath::with_field("obj", "field"),
false,
Some(20),
None,
);
assert!(!map.is_tainted_dotted("obj.field"));
}
#[test]
fn test_property_read() {
let mut map = FieldTaintMap::new();
map.mark_tainted_dotted("obj.field", Some(10), None);
let path = FieldPath::with_field("obj", "field");
assert!(map.handle_property_read(&path));
let path2 = FieldPath::with_field("obj", "other");
assert!(!map.handle_property_read(&path2));
}
#[test]
fn test_destructuring() {
let mut map = FieldTaintMap::new();
map.mark_tainted_dotted("obj.tainted_field", Some(10), None);
map.mark_clean(&FieldPath::with_field("obj", "clean_field"));
let source = FieldPath::new("obj");
let result = map.handle_destructuring(&source, &["tainted_field", "clean_field"]);
assert_eq!(result.get("tainted_field"), Some(&true));
assert_eq!(result.get("clean_field"), Some(&false));
}
#[test]
fn test_spread_with_override() {
let mut map = FieldTaintMap::new();
map.mark_tainted_dotted("src.tainted", Some(10), None);
map.mark_tainted_dotted("src.overridden", Some(10), None);
let source = FieldPath::new("src");
let mut overrides = HashMap::new();
overrides.insert("overridden".to_string(), false); overrides.insert("new_tainted".to_string(), true);
let result = map.handle_spread_with_override(&source, &overrides, "dest", Some(20));
assert!(result.is_tainted_dotted("dest.tainted"));
assert!(!result.is_tainted_dotted("dest.overridden")); assert!(result.is_tainted_dotted("dest.new_tainted"));
}
#[test]
fn test_computed_access() {
let mut map = FieldTaintMap::new();
map.mark_tainted_dotted("obj.secret", Some(10), None);
let source = FieldPath::new("obj");
assert!(map.handle_computed_access(&source, Some("secret")));
assert!(!map.handle_computed_access(&source, Some("other")));
assert!(map.handle_computed_access(&source, None));
}
#[test]
fn test_merge_maps() {
let mut map1 = FieldTaintMap::new();
map1.mark_tainted_dotted("obj.a", Some(10), None);
let mut map2 = FieldTaintMap::new();
map2.mark_tainted_dotted("obj.b", Some(20), None);
map1.merge(&map2);
assert!(map1.is_tainted_dotted("obj.a"));
assert!(map1.is_tainted_dotted("obj.b"));
}
#[test]
fn test_field_sensitive_analyzer() {
let mut analyzer = FieldSensitiveAnalyzer::new();
analyzer.process_property_assignment("obj", "userInput", None, true, 10);
analyzer.process_property_assignment("obj", "safe", None, false, 11);
assert!(analyzer.is_tainted("obj.userInput"));
assert!(!analyzer.is_tainted("obj.safe"));
let is_tainted = analyzer.process_property_read("obj", "userInput");
assert!(is_tainted);
}
#[test]
fn test_analyzer_destructuring() {
let mut analyzer = FieldSensitiveAnalyzer::new();
analyzer.process_property_assignment("source", "tainted", None, true, 10);
analyzer.process_destructuring("source", &[("tainted", "x"), ("clean", "y")], 20);
assert!(analyzer.is_tainted("x"));
assert!(!analyzer.is_tainted("y"));
}
#[test]
fn test_analyzer_spread() {
let mut analyzer = FieldSensitiveAnalyzer::new();
analyzer.process_property_assignment("src", "existing", None, true, 10);
analyzer.process_spread_with_override(
"src",
vec![("override", true), ("clean", false)],
"dest",
20,
);
assert!(analyzer.is_tainted("dest.existing"));
assert!(analyzer.is_tainted("dest.override"));
assert!(!analyzer.is_tainted("dest.clean"));
}
#[test]
fn test_fully_tainted_variable() {
let mut analyzer = FieldSensitiveAnalyzer::new();
analyzer.mark_fully_tainted("userInput", 10, Some("req.body".to_string()));
assert!(analyzer.is_tainted("userInput"));
assert!(analyzer.is_tainted("userInput.anything"));
assert!(analyzer.is_tainted("userInput.deep.nested.field"));
}
#[test]
fn test_build_result() {
let mut analyzer = FieldSensitiveAnalyzer::new();
analyzer.process_property_assignment("obj", "field", None, true, 10);
analyzer.mark_fully_tainted("tainted_var", 20, None);
let result = analyzer.build();
assert!(result.is_tainted("obj.field"));
assert!(result.is_tainted("tainted_var"));
assert!(result.is_tainted("tainted_var.any_field"));
let all_tainted = result.all_tainted_paths();
assert!(!all_tainted.is_empty());
}
#[test]
fn test_field_taint_status() {
assert!(FieldTaintStatus::Tainted.is_tainted());
assert!(!FieldTaintStatus::Tainted.is_clean());
assert!(!FieldTaintStatus::Clean.is_tainted());
assert!(FieldTaintStatus::Clean.is_clean());
assert!(!FieldTaintStatus::Sanitized.is_tainted());
assert!(FieldTaintStatus::Sanitized.is_clean());
assert!(!FieldTaintStatus::Unknown.is_tainted());
assert!(!FieldTaintStatus::Unknown.is_clean());
}
#[test]
fn test_tainted_fields_of() {
let mut map = FieldTaintMap::new();
map.mark_tainted_dotted("obj.a", Some(10), None);
map.mark_tainted_dotted("obj.b", Some(20), None);
map.mark_tainted_dotted("other.c", Some(30), None);
let obj_fields = map.tainted_fields_of("obj");
assert_eq!(obj_fields.len(), 2);
let other_fields = map.tainted_fields_of("other");
assert_eq!(other_fields.len(), 1);
let empty_fields = map.tainted_fields_of("nonexistent");
assert_eq!(empty_fields.len(), 0);
}
#[test]
fn test_array_destructuring() {
let mut map = FieldTaintMap::new();
map.mark_tainted(FieldPath::with_field("arr", "0"), Some(10), None);
map.mark_tainted(FieldPath::with_field("arr", "2"), Some(20), None);
let source = FieldPath::new("arr");
let results = map.handle_array_destructuring(&source, 4);
assert_eq!(results, vec![true, false, true, false]);
}
#[test]
fn test_sanitization() {
let mut map = FieldTaintMap::new();
map.mark_tainted_dotted("obj.field", Some(10), Some("userInput".to_string()));
assert!(map.is_tainted_dotted("obj.field"));
map.mark_sanitized(&FieldPath::with_field("obj", "field"), 20);
assert!(!map.is_tainted_dotted("obj.field"));
let info = map
.get_info(&FieldPath::with_field("obj", "field"))
.unwrap();
assert_eq!(info.status, FieldTaintStatus::Sanitized);
assert_eq!(info.sanitized_line, Some(20));
}
}