Skip to main content

jsdet_core/
taint.rs

1//! Taint tracking for the jsdet core.
2//!
3//! This module provides real taint tracking in the Rust core, complementing
4//! the JavaScript-level taint tracking in the QuickJS engine.
5//!
6//! # Architecture
7//!
8//! The taint system has two layers:
9//!
10//! 1. **JS Engine Layer** (QuickJS/WASM): Taint is stored on JS string objects
11//!    via the `__jsdet_set_taint`/`__jsdet_get_taint` intrinsics. This tracks
12//!    taint through JS operations like concat, slice, replace.
13//!
14//! 2. **Rust Core Layer** (this module): Taint is stored on [`Value`] objects
15//!    via [`TaintLabel`]. This tracks taint as values pass through the bridge
16//!    between JS and Rust.
17//!
18//! # Usage
19//!
20//! ```
21//! use jsdet_core::taint::{TaintLabel, TaintTracker};
22//! use jsdet_core::observation::Value;
23//!
24//! // Create a tracker
25//! let mut tracker = TaintTracker::new();
26//!
27//! // Mark a source as tainted
28//! let tainted_value = Value::tainted_string("attacker_input", TaintLabel::new(1));
29//!
30//! // Check if value reaches a sink
31//! if let Some(flow) = Value::check_taint_at_sink("eval", &[tainted_value]) {
32//!     println!("Taint flow detected: {} -> {}", flow.sink, flow.label.0);
33//! }
34//! ```
35
36use crate::observation::{TaintFlow, TaintLabel, Value};
37use std::collections::HashMap;
38
39/// A taint source registration.
40#[derive(Debug, Clone)]
41pub struct Source {
42    /// The API name that produces tainted data (e.g., "chrome.runtime.onMessage").
43    pub api: String,
44    /// The taint label to assign.
45    pub label: TaintLabel,
46    /// Human-readable description.
47    pub description: String,
48}
49
50/// A taint sink registration.
51#[derive(Debug, Clone)]
52pub struct Sink {
53    /// The API name that is dangerous (e.g., "eval", "chrome.tabs.executeScript").
54    pub api: String,
55    /// Which argument positions are dangerous (0-indexed).
56    pub dangerous_args: Vec<usize>,
57    /// Severity if tainted data reaches here.
58    pub severity: Severity,
59    /// CWE identifier.
60    pub cwe: String,
61}
62
63/// Severity levels for taint flows.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum Severity {
66    Critical,
67    High,
68    Medium,
69    Low,
70    Info,
71}
72
73impl std::fmt::Display for Severity {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            Self::Critical => write!(f, "critical"),
77            Self::High => write!(f, "high"),
78            Self::Medium => write!(f, "medium"),
79            Self::Low => write!(f, "low"),
80            Self::Info => write!(f, "info"),
81        }
82    }
83}
84
85/// Tracks taint through a single execution session.
86///
87/// This is the main interface for cross-function taint tracking in the Rust core.
88/// It maintains the set of sources and sinks, and records confirmed taint flows.
89#[derive(Debug, Default)]
90pub struct TaintTracker {
91    /// Registered taint sources.
92    sources: HashMap<String, Source>,
93    /// Registered taint sinks.
94    sinks: HashMap<String, Sink>,
95    /// Confirmed taint flows (tainted data reached a sink).
96    flows: Vec<TaintFlow>,
97    /// Next taint label ID to assign.
98    next_label: u32,
99}
100
101impl TaintTracker {
102    /// Create a new empty taint tracker.
103    pub fn new() -> Self {
104        Self {
105            sources: HashMap::new(),
106            sinks: HashMap::new(),
107            flows: Vec::new(),
108            next_label: 1, // Start at 1 (0 = clean)
109        }
110    }
111
112    /// Register a new taint source.
113    ///
114    /// Returns the assigned taint label for this source.
115    /// CRITICAL FIX: Uses saturating arithmetic to prevent overflow.
116    pub fn register_source(
117        &mut self,
118        api: impl Into<String>,
119        description: impl Into<String>,
120    ) -> TaintLabel {
121        let label = TaintLabel::new(self.next_label);
122        // CRITICAL FIX: Use saturating_add to prevent overflow panic
123        self.next_label = self.next_label.saturating_add(1);
124        // Ensure we never return label 0 (CLEAN) even after overflow
125        if label.0 == 0 {
126            return TaintLabel::new(1);
127        }
128
129        let source = Source {
130            api: api.into(),
131            label,
132            description: description.into(),
133        };
134
135        self.sources.insert(source.api.clone(), source);
136        label
137    }
138
139    /// Register a new taint sink.
140    pub fn register_sink(
141        &mut self,
142        api: impl Into<String>,
143        dangerous_args: Vec<usize>,
144        severity: Severity,
145        cwe: impl Into<String>,
146    ) {
147        let sink = Sink {
148            api: api.into(),
149            dangerous_args,
150            severity,
151            cwe: cwe.into(),
152        };
153        self.sinks.insert(sink.api.clone(), sink);
154    }
155
156    /// Check if an API is a registered source.
157    pub fn is_source(&self, api: &str) -> Option<&Source> {
158        self.sources.get(api)
159    }
160
161    /// Check if an API is a registered sink.
162    pub fn is_sink(&self, api: &str) -> Option<&Sink> {
163        self.sinks.get(api)
164    }
165
166    /// Apply taint to a value returned from a source API.
167    ///
168    /// If the API is a registered source, the value is marked with the
169    /// corresponding taint label. Otherwise, the value is returned unchanged.
170    pub fn apply_source_taint(&self, api: &str, value: Value) -> Value {
171        if let Some(source) = self.is_source(api) {
172            value.with_taint(source.label)
173        } else {
174            value
175        }
176    }
177
178    /// Check for taint flows at a sink API call.
179    ///
180    /// If any of the dangerous arguments are tainted, records a taint flow
181    /// and returns it. Returns None if no tainted data reached the sink.
182    pub fn check_sink(&mut self, api: &str, args: &[Value]) -> Option<TaintFlow> {
183        let sink = self.is_sink(api)?;
184
185        // Check only the dangerous argument positions
186        let dangerous_values: Vec<(usize, &Value)> = args
187            .iter()
188            .enumerate()
189            .filter(|(idx, _)| sink.dangerous_args.contains(idx))
190            .collect();
191
192        if let Some(flow) = Value::check_taint_at_sink(
193            api,
194            &dangerous_values
195                .iter()
196                .map(|(_, v)| (*v).clone())
197                .collect::<Vec<_>>(),
198        ) {
199            self.flows.push(flow.clone());
200            Some(flow)
201        } else {
202            None
203        }
204    }
205
206    /// Get all recorded taint flows.
207    pub fn flows(&self) -> &[TaintFlow] {
208        &self.flows
209    }
210
211    /// Take all recorded taint flows (clears internal list).
212    pub fn take_flows(&mut self) -> Vec<TaintFlow> {
213        std::mem::take(&mut self.flows)
214    }
215
216    /// Returns true if any taint flows were recorded.
217    pub fn has_flows(&self) -> bool {
218        !self.flows.is_empty()
219    }
220
221    /// Count of confirmed taint flows.
222    pub fn flow_count(&self) -> usize {
223        self.flows.len()
224    }
225
226    /// Get all registered sources.
227    pub fn sources(&self) -> &HashMap<String, Source> {
228        &self.sources
229    }
230
231    /// Get all registered sinks.
232    pub fn sinks(&self) -> &HashMap<String, Sink> {
233        &self.sinks
234    }
235}
236
237/// Propagate taint through a string concatenation operation.
238///
239/// Takes multiple values and returns a new string Value with the combined
240/// taint labels. If any input is tainted, the result is tainted.
241///
242/// # Example
243///
244/// ```
245/// use jsdet_core::taint::propagate_concat;
246/// use jsdet_core::observation::{Value, TaintLabel};
247///
248/// let a = Value::string("hello ");
249/// let b = Value::tainted_string("world", TaintLabel::new(1));
250///
251/// let result = propagate_concat(&[a, b]).unwrap();
252/// assert!(result.is_tainted());
253/// assert_eq!(result.as_str(), Some("hello world"));
254/// ```
255pub fn propagate_concat(values: &[Value]) -> Option<Value> {
256    if values.is_empty() {
257        return Some(Value::string(""));
258    }
259
260    // Build the concatenated string
261    let mut result = String::new();
262    let mut combined_label = TaintLabel::CLEAN;
263
264    for value in values {
265        match value {
266            Value::String(s, label) => {
267                result.push_str(s);
268                if combined_label.is_clean() && label.is_tainted() {
269                    combined_label = *label;
270                }
271            }
272            _ => return None, // Non-string in concat
273        }
274    }
275
276    Some(Value::String(result, combined_label))
277}
278
279/// Propagate taint through a string slice operation.
280///
281/// The result carries the same taint label as the source.
282pub fn propagate_slice(value: &Value, start: usize, end: usize) -> Option<Value> {
283    value.slice(start, end)
284}
285
286/// Propagate taint through a string replace operation.
287///
288/// The result carries the taint label of the source string.
289pub fn propagate_replace(value: &Value, from: &str, to: &str) -> Option<Value> {
290    value.replace(from, to)
291}
292
293/// Propagate taint through JSON.parse.
294///
295/// If the input JSON string is tainted, the resulting Json value
296/// is also tainted.
297pub fn propagate_json_parse(value: &Value) -> Option<Value> {
298    match value {
299        Value::String(s, label) => Some(Value::Json(s.clone(), *label)),
300        _ => None,
301    }
302}
303
304/// Propagate taint through JSON.stringify.
305///
306/// If the input value is tainted, the resulting string is also tainted.
307pub fn propagate_json_stringify(value: &Value) -> Option<Value> {
308    match value {
309        Value::Json(s, label) => Some(Value::String(s.clone(), *label)),
310        _ => None,
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn taint_label_basic() {
320        let clean = TaintLabel::CLEAN;
321        assert!(!clean.is_tainted());
322        assert!(clean.is_clean());
323
324        let tainted = TaintLabel::new(1);
325        assert!(tainted.is_tainted());
326        assert!(!tainted.is_clean());
327    }
328
329    #[test]
330    fn taint_label_combine() {
331        let clean = TaintLabel::CLEAN;
332        let t1 = TaintLabel::new(1);
333        let t2 = TaintLabel::new(2);
334
335        assert_eq!(clean.combine(clean), clean);
336        assert_eq!(clean.combine(t1), t1);
337        assert_eq!(t1.combine(clean), t1);
338        assert_eq!(t1.combine(t2), t1); // First wins when both tainted
339    }
340
341    #[test]
342    fn value_taint_tracking() {
343        let clean = Value::string("clean");
344        let tainted = Value::tainted_string("tainted", TaintLabel::new(1));
345
346        assert!(!clean.is_tainted());
347        assert!(tainted.is_tainted());
348
349        assert_eq!(clean.taint_label(), TaintLabel::CLEAN);
350        assert_eq!(tainted.taint_label(), TaintLabel::new(1));
351    }
352
353    #[test]
354    fn value_with_taint() {
355        let clean = Value::string("test");
356        let tainted = clean.with_taint(TaintLabel::new(5));
357
358        assert!(tainted.is_tainted());
359        assert_eq!(tainted.taint_label(), TaintLabel::new(5));
360    }
361
362    #[test]
363    fn check_taint_at_sink_detects_tainted() {
364        let args = vec![
365            Value::string("safe"),
366            Value::tainted_string("dangerous", TaintLabel::new(1)),
367        ];
368
369        let flow = Value::check_taint_at_sink("eval", &args);
370        assert!(flow.is_some());
371
372        let flow = flow.unwrap();
373        assert_eq!(flow.sink, "eval");
374        assert_eq!(flow.label, TaintLabel::new(1));
375        assert_eq!(flow.tainted_args, vec![1]);
376    }
377
378    #[test]
379    fn check_taint_at_sink_returns_none_for_clean() {
380        let args = vec![Value::string("safe1"), Value::string("safe2")];
381
382        let flow = Value::check_taint_at_sink("eval", &args);
383        assert!(flow.is_none());
384    }
385
386    #[test]
387    fn value_concat_propagates_taint() {
388        let a = Value::string("hello ");
389        let b = Value::tainted_string("world", TaintLabel::new(1));
390
391        let result = a.concat(&b).unwrap();
392
393        assert!(result.is_tainted());
394        assert_eq!(result.taint_label(), TaintLabel::new(1));
395        assert_eq!(result.as_str(), Some("hello world"));
396    }
397
398    #[test]
399    fn value_slice_preserves_taint() {
400        let s = Value::tainted_string("abcdef", TaintLabel::new(2));
401
402        let result = s.slice(1, 4).unwrap();
403
404        assert!(result.is_tainted());
405        assert_eq!(result.taint_label(), TaintLabel::new(2));
406        assert_eq!(result.as_str(), Some("bcd"));
407    }
408
409    #[test]
410    fn value_replace_preserves_taint() {
411        let s = Value::tainted_string("hello world", TaintLabel::new(3));
412
413        let result = s.replace("world", "universe").unwrap();
414
415        assert!(result.is_tainted());
416        assert_eq!(result.taint_label(), TaintLabel::new(3));
417        assert_eq!(result.as_str(), Some("hello universe"));
418    }
419
420    #[test]
421    fn value_equality_ignores_taint() {
422        let a = Value::string("test");
423        let b = Value::tainted_string("test", TaintLabel::new(1));
424
425        // Equality ignores taint - only the value matters
426        assert_eq!(a, b);
427
428        // But taint status is different
429        assert!(!a.is_tainted());
430        assert!(b.is_tainted());
431    }
432
433    #[test]
434    fn taint_tracker_registration() {
435        let mut tracker = TaintTracker::new();
436
437        let label = tracker.register_source("chrome.runtime.onMessage", "Message from extension");
438        assert_eq!(label, TaintLabel::new(1));
439
440        tracker.register_sink("eval", vec![0], Severity::Critical, "CWE-95");
441
442        assert!(tracker.is_source("chrome.runtime.onMessage").is_some());
443        assert!(tracker.is_sink("eval").is_some());
444        assert!(tracker.is_source("fetch").is_none());
445    }
446
447    #[test]
448    fn taint_tracker_detects_flow() {
449        let mut tracker = TaintTracker::new();
450
451        tracker.register_source("source.api", "Test source");
452        tracker.register_sink("sink.api", vec![0], Severity::High, "CWE-79");
453
454        // Apply source taint (should apply the label)
455        let tainted = tracker.apply_source_taint("source.api", Value::string("evil"));
456        assert!(tainted.is_tainted());
457
458        // Check at sink
459        let flow = tracker.check_sink("sink.api", &[tainted]);
460        assert!(flow.is_some());
461        assert_eq!(tracker.flow_count(), 1);
462    }
463
464    #[test]
465    fn propagate_concat_multiple() {
466        let values = vec![
467            Value::string("a"),
468            Value::tainted_string("b", TaintLabel::new(1)),
469            Value::string("c"),
470        ];
471
472        let result = propagate_concat(&values).unwrap();
473        assert!(result.is_tainted());
474        assert_eq!(result.as_str(), Some("abc"));
475    }
476
477    #[test]
478    fn propagate_concat_empty() {
479        let result = propagate_concat(&[]).unwrap();
480        assert_eq!(result.as_str(), Some(""));
481        assert!(!result.is_tainted());
482    }
483
484    #[test]
485    fn propagate_json_parse_stringifies() {
486        let json_str = Value::tainted_string(r#"{"key":"value"}"#, TaintLabel::new(1));
487
488        let parsed = propagate_json_parse(&json_str).unwrap();
489        assert!(matches!(parsed, Value::Json(_, _)));
490        assert!(parsed.is_tainted());
491    }
492
493    #[test]
494    fn propagate_json_stringify_preserves_taint() {
495        let json = Value::tainted_json(r#"{"key":"value"}"#, TaintLabel::new(2));
496
497        let stringified = propagate_json_stringify(&json).unwrap();
498        assert!(matches!(stringified, Value::String(_, _)));
499        assert!(stringified.is_tainted());
500        assert_eq!(stringified.taint_label(), TaintLabel::new(2));
501    }
502}