#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum Observation {
ApiCall {
api: String,
args: Vec<Value>,
result: Value,
},
PropertyRead {
object: String,
property: String,
value: Value,
},
PropertyWrite {
object: String,
property: String,
value: Value,
},
DomMutation {
kind: DomMutationKind,
target: String,
detail: String,
},
NetworkRequest {
url: String,
method: String,
headers: Vec<(String, String)>,
body: Option<String>,
},
TimerSet {
id: u32,
delay_ms: u32,
is_interval: bool,
callback_preview: String,
},
DynamicCodeExec {
source: DynamicCodeSource,
code_preview: String,
},
CookieAccess {
operation: CookieOp,
name: String,
value: Option<String>,
},
CssExfiltration {
selector: String,
url: String,
trigger: String,
},
WasmInstantiation {
module_size: usize,
import_names: Vec<String>,
export_names: Vec<String>,
},
FingerprintAccess { api: String, detail: String },
ContextMessage {
from_context: String,
to_context: String,
payload: Value,
},
Error {
message: String,
script_index: Option<usize>,
},
ResourceLimit {
kind: ResourceLimitKind,
detail: String,
},
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum DomMutationKind {
ElementCreated,
ChildAppended,
ChildRemoved,
AttributeSet,
AttributeRemoved,
StyleMutation,
ClassMutation,
TextMutation,
InnerHtmlSet,
DocumentWrite,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum DynamicCodeSource {
Eval,
Function,
SetTimeoutString,
SetIntervalString,
ImportScripts,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum CookieOp {
Read,
Write,
Delete,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum ResourceLimitKind {
Fuel,
Memory,
Timeout,
ObservationCount,
ScriptCount,
StackDepth,
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
)]
pub struct TaintLabel(pub u32);
impl TaintLabel {
pub const CLEAN: Self = Self(0);
#[must_use]
pub fn new(id: u32) -> Self {
Self(id)
}
#[must_use]
pub fn is_clean(self) -> bool {
self == Self::CLEAN
}
#[must_use]
pub fn is_tainted(self) -> bool {
!self.is_clean()
}
#[must_use]
pub fn combine(self, other: Self) -> Self {
if self.is_tainted() { self } else { other }
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct TaintFlow {
pub sink: String,
pub label: TaintLabel,
pub tainted_args: Vec<usize>,
}
pub type TaintedValue = Value;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum Value {
Undefined,
Null,
Bool(bool),
Int(i64),
Float(f64),
String(String, TaintLabel),
Json(String, TaintLabel),
Bytes(Vec<u8>),
}
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Undefined, Self::Undefined) | (Self::Null, Self::Null) => true,
(Self::Bool(a), Self::Bool(b)) => a == b,
(Self::Int(a), Self::Int(b)) => a == b,
(Self::Float(a), Self::Float(b)) => a.to_bits() == b.to_bits(),
(Self::String(a, _), Self::String(b, _)) | (Self::Json(a, _), Self::Json(b, _)) => {
a == b
}
(Self::Bytes(a), Self::Bytes(b)) => a == b,
_ => false,
}
}
}
impl Value {
#[must_use]
pub fn string(value: impl Into<String>) -> Self {
Self::String(value.into(), TaintLabel::CLEAN)
}
#[must_use]
pub fn tainted_string(value: impl Into<String>, label: TaintLabel) -> Self {
Self::String(value.into(), label)
}
#[must_use]
pub fn json(value: impl Into<String>) -> Self {
Self::Json(value.into(), TaintLabel::CLEAN)
}
#[must_use]
pub fn tainted_json(value: impl Into<String>, label: TaintLabel) -> Self {
Self::Json(value.into(), label)
}
#[must_use]
pub fn is_nullish(&self) -> bool {
matches!(self, Self::Undefined | Self::Null)
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Self::String(s, _) | Self::Json(s, _) => Some(s),
_ => None,
}
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(b) => Some(*b),
_ => None,
}
}
#[must_use]
pub fn taint_label(&self) -> TaintLabel {
match self {
Self::String(_, label) | Self::Json(_, label) => *label,
_ => TaintLabel::CLEAN,
}
}
#[must_use]
pub fn is_tainted(&self) -> bool {
self.taint_label().is_tainted()
}
#[must_use]
pub fn with_taint(self, label: TaintLabel) -> Self {
match self {
Self::String(s, _) => Self::String(s, label),
Self::Json(s, _) => Self::Json(s, label),
other => other,
}
}
pub fn concat(&self, other: &Self) -> Option<Self> {
match (self, other) {
(Self::String(a, left), Self::String(b, right)) => {
Some(Self::String(format!("{a}{b}"), left.combine(*right)))
}
_ => None,
}
}
pub fn slice(&self, start: usize, end: usize) -> Option<Self> {
match self {
Self::String(s, label) => {
let chars: Vec<char> = s.chars().collect();
let start = start.min(chars.len());
let end = end.min(chars.len()).max(start);
Some(Self::String(chars[start..end].iter().collect(), *label))
}
_ => None,
}
}
pub fn replace(&self, from: &str, to: &str) -> Option<Self> {
match self {
Self::String(s, label) => Some(Self::String(s.replace(from, to), *label)),
_ => None,
}
}
#[must_use]
pub fn check_taint_at_sink(sink: &str, args: &[Self]) -> Option<TaintFlow> {
let tainted_args: Vec<usize> = args
.iter()
.enumerate()
.filter_map(|(idx, value)| value.is_tainted().then_some(idx))
.collect();
let first = tainted_args.first().copied()?;
Some(TaintFlow {
sink: sink.to_string(),
label: args[first].taint_label(),
tainted_args,
})
}
}
impl std::fmt::Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Undefined => write!(f, "undefined"),
Self::Null => write!(f, "null"),
Self::Bool(b) => write!(f, "{b}"),
Self::Int(n) => write!(f, "{n}"),
Self::Float(n) => write!(f, "{n}"),
Self::String(s, _) => write!(f, "{s:?}"),
Self::Json(j, _) => write!(f, "{j}"),
Self::Bytes(b) => write!(f, "<{} bytes>", b.len()),
}
}
}