1#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
10pub enum Observation {
11 ApiCall {
13 api: String,
14 args: Vec<Value>,
15 result: Value,
16 },
17 PropertyRead {
19 object: String,
20 property: String,
21 value: Value,
22 },
23 PropertyWrite {
25 object: String,
26 property: String,
27 value: Value,
28 },
29 DomMutation {
31 kind: DomMutationKind,
32 target: String,
33 detail: String,
34 },
35 NetworkRequest {
37 url: String,
38 method: String,
39 headers: Vec<(String, String)>,
40 body: Option<String>,
41 },
42 TimerSet {
44 id: u32,
45 delay_ms: u32,
46 is_interval: bool,
47 callback_preview: String,
48 },
49 DynamicCodeExec {
51 source: DynamicCodeSource,
52 code_preview: String,
53 },
54 CookieAccess {
56 operation: CookieOp,
57 name: String,
58 value: Option<String>,
59 },
60 CssExfiltration {
62 selector: String,
63 url: String,
64 trigger: String,
65 },
66 WasmInstantiation {
68 module_size: usize,
69 import_names: Vec<String>,
70 export_names: Vec<String>,
71 },
72 FingerprintAccess { api: String, detail: String },
74 ContextMessage {
76 from_context: String,
77 to_context: String,
78 payload: Value,
79 },
80 Error {
82 message: String,
83 script_index: Option<usize>,
84 },
85 ResourceLimit {
87 kind: ResourceLimitKind,
88 detail: String,
89 },
90}
91
92#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
94pub enum DomMutationKind {
95 ElementCreated,
96 ChildAppended,
97 ChildRemoved,
98 AttributeSet,
99 AttributeRemoved,
100 StyleMutation,
101 ClassMutation,
102 TextMutation,
103 InnerHtmlSet,
104 DocumentWrite,
105}
106
107#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
109pub enum DynamicCodeSource {
110 Eval,
111 Function,
112 SetTimeoutString,
113 SetIntervalString,
114 ImportScripts,
115}
116
117#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
119pub enum CookieOp {
120 Read,
121 Write,
122 Delete,
123}
124
125#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
127pub enum ResourceLimitKind {
128 Fuel,
129 Memory,
130 Timeout,
131 ObservationCount,
132 ScriptCount,
133 StackDepth,
134}
135
136#[derive(
138 Debug, Clone, Copy, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
139)]
140pub struct TaintLabel(pub u32);
141
142impl TaintLabel {
143 pub const CLEAN: Self = Self(0);
144
145 #[must_use]
146 pub fn new(id: u32) -> Self {
147 Self(id)
148 }
149
150 #[must_use]
151 pub fn is_clean(self) -> bool {
152 self == Self::CLEAN
153 }
154
155 #[must_use]
156 pub fn is_tainted(self) -> bool {
157 !self.is_clean()
158 }
159
160 #[must_use]
161 pub fn combine(self, other: Self) -> Self {
162 if self.is_tainted() { self } else { other }
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
168pub struct TaintFlow {
169 pub sink: String,
170 pub label: TaintLabel,
171 pub tainted_args: Vec<usize>,
172}
173
174pub type TaintedValue = Value;
176
177#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
179pub enum Value {
180 Undefined,
181 Null,
182 Bool(bool),
183 Int(i64),
184 Float(f64),
185 String(String, TaintLabel),
186 Json(String, TaintLabel),
188 Bytes(Vec<u8>),
190}
191
192impl PartialEq for Value {
193 fn eq(&self, other: &Self) -> bool {
194 match (self, other) {
195 (Self::Undefined, Self::Undefined) | (Self::Null, Self::Null) => true,
196 (Self::Bool(a), Self::Bool(b)) => a == b,
197 (Self::Int(a), Self::Int(b)) => a == b,
198 (Self::Float(a), Self::Float(b)) => a.to_bits() == b.to_bits(),
199 (Self::String(a, _), Self::String(b, _)) | (Self::Json(a, _), Self::Json(b, _)) => {
200 a == b
201 }
202 (Self::Bytes(a), Self::Bytes(b)) => a == b,
203 _ => false,
204 }
205 }
206}
207
208impl Value {
209 #[must_use]
210 pub fn string(value: impl Into<String>) -> Self {
211 Self::String(value.into(), TaintLabel::CLEAN)
212 }
213
214 #[must_use]
215 pub fn tainted_string(value: impl Into<String>, label: TaintLabel) -> Self {
216 Self::String(value.into(), label)
217 }
218
219 #[must_use]
220 pub fn json(value: impl Into<String>) -> Self {
221 Self::Json(value.into(), TaintLabel::CLEAN)
222 }
223
224 #[must_use]
225 pub fn tainted_json(value: impl Into<String>, label: TaintLabel) -> Self {
226 Self::Json(value.into(), label)
227 }
228
229 #[must_use]
230 pub fn is_nullish(&self) -> bool {
231 matches!(self, Self::Undefined | Self::Null)
232 }
233
234 #[must_use]
235 pub fn as_str(&self) -> Option<&str> {
236 match self {
237 Self::String(s, _) | Self::Json(s, _) => Some(s),
238 _ => None,
239 }
240 }
241
242 #[must_use]
243 pub fn as_bool(&self) -> Option<bool> {
244 match self {
245 Self::Bool(b) => Some(*b),
246 _ => None,
247 }
248 }
249
250 #[must_use]
251 pub fn taint_label(&self) -> TaintLabel {
252 match self {
253 Self::String(_, label) | Self::Json(_, label) => *label,
254 _ => TaintLabel::CLEAN,
255 }
256 }
257
258 #[must_use]
259 pub fn is_tainted(&self) -> bool {
260 self.taint_label().is_tainted()
261 }
262
263 #[must_use]
264 pub fn with_taint(self, label: TaintLabel) -> Self {
265 match self {
266 Self::String(s, _) => Self::String(s, label),
267 Self::Json(s, _) => Self::Json(s, label),
268 other => other,
269 }
270 }
271
272 pub fn concat(&self, other: &Self) -> Option<Self> {
273 match (self, other) {
274 (Self::String(a, left), Self::String(b, right)) => {
275 Some(Self::String(format!("{a}{b}"), left.combine(*right)))
276 }
277 _ => None,
278 }
279 }
280
281 pub fn slice(&self, start: usize, end: usize) -> Option<Self> {
282 match self {
283 Self::String(s, label) => {
284 let chars: Vec<char> = s.chars().collect();
285 let start = start.min(chars.len());
286 let end = end.min(chars.len()).max(start);
287 Some(Self::String(chars[start..end].iter().collect(), *label))
288 }
289 _ => None,
290 }
291 }
292
293 pub fn replace(&self, from: &str, to: &str) -> Option<Self> {
294 match self {
295 Self::String(s, label) => Some(Self::String(s.replace(from, to), *label)),
296 _ => None,
297 }
298 }
299
300 #[must_use]
301 pub fn check_taint_at_sink(sink: &str, args: &[Self]) -> Option<TaintFlow> {
302 let tainted_args: Vec<usize> = args
303 .iter()
304 .enumerate()
305 .filter_map(|(idx, value)| value.is_tainted().then_some(idx))
306 .collect();
307 let first = tainted_args.first().copied()?;
308 Some(TaintFlow {
309 sink: sink.to_string(),
310 label: args[first].taint_label(),
311 tainted_args,
312 })
313 }
314}
315
316impl std::fmt::Display for Value {
317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318 match self {
319 Self::Undefined => write!(f, "undefined"),
320 Self::Null => write!(f, "null"),
321 Self::Bool(b) => write!(f, "{b}"),
322 Self::Int(n) => write!(f, "{n}"),
323 Self::Float(n) => write!(f, "{n}"),
324 Self::String(s, _) => write!(f, "{s:?}"),
325 Self::Json(j, _) => write!(f, "{j}"),
326 Self::Bytes(b) => write!(f, "<{} bytes>", b.len()),
327 }
328 }
329}