use std::sync::{Arc, Mutex};
use jsdet_core::bridge::Bridge;
use jsdet_core::observation::{Observation, Value};
use crate::manifest::Manifest;
use crate::profile::AnalysisProfile;
use crate::state::ExtensionState;
pub struct ChromeExtBridge {
manifest: Manifest,
profile: AnalysisProfile,
state: ExtensionState,
observations: Arc<Mutex<Vec<Observation>>>,
}
impl ChromeExtBridge {
pub fn new(manifest: Manifest, profile: AnalysisProfile, state: ExtensionState) -> Self {
Self {
manifest,
profile,
state,
observations: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn take_observations(&self) -> Vec<Observation> {
let mut guard = self
.observations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
std::mem::take(&mut *guard)
}
fn observe(&self, obs: Observation) {
if let Ok(mut guard) = self.observations.lock() {
guard.push(obs);
}
}
fn handle_tabs(&self, method: &str, args: &[Value]) -> Result<Value, String> {
match method {
"query" => {
let guard = self
.state
.tabs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let json = serde_json::to_string(&*guard).unwrap_or_default();
Ok(Value::json(json))
}
"create" => {
self.observe(Observation::ApiCall {
api: "chrome.tabs.create".to_string(),
args: args.to_vec(),
result: Value::json(r#"{"id": 999}"#),
});
Ok(Value::json(r#"{"id": 999}"#))
}
"update" => {
self.observe(Observation::ApiCall {
api: "chrome.tabs.update".into(),
args: args.to_vec(),
result: Value::Null,
});
if let Some(sink) = self.profile.is_sink("chrome.tabs.update") {
self.observe(Observation::ApiCall {
api: format!("SINK:chrome.tabs.update [{}]", sink.cwe),
args: args.to_vec(),
result: Value::Null,
});
}
Ok(Value::Null)
}
"executeScript" | "sendMessage" => {
self.observe(Observation::ApiCall {
api: format!("chrome.tabs.{method}"),
args: args.to_vec(),
result: Value::Null,
});
if let Some(sink) = self.profile.is_sink(&format!("chrome.tabs.{method}")) {
self.observe(Observation::ApiCall {
api: format!("SINK:chrome.tabs.{method} [{}]", sink.cwe),
args: args.to_vec(),
result: Value::Null,
});
}
Ok(Value::Null)
}
_ => Err(format!("chrome.tabs.{method} is not defined")),
}
}
fn handle_cookies(&self, method: &str, args: &[Value]) -> Result<Value, String> {
match method {
"getAll" | "get" => {
let guard = self
.state
.cookies
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let json = serde_json::to_string(&*guard).unwrap_or_default();
self.observe(Observation::ApiCall {
api: format!("chrome.cookies.{method}"),
args: args.to_vec(),
result: Value::json(json.clone()),
});
Ok(Value::json(json))
}
"set" | "remove" => {
self.observe(Observation::ApiCall {
api: format!("chrome.cookies.{method}"),
args: args.to_vec(),
result: Value::Null,
});
Ok(Value::Null)
}
_ => Err(format!("chrome.cookies.{method} is not defined")),
}
}
fn handle_storage(&self, method: &str, args: &[Value]) -> Result<Value, String> {
let (area, op) = if method.contains('.') {
let parts: Vec<&str> = method.splitn(2, '.').collect();
(parts[0], parts[1])
} else {
("local", method)
};
let storage = match area {
"sync" => &self.state.storage_sync,
_ => &self.state.storage_local,
};
match op {
"get" => {
let data = storage.lock().unwrap();
let json = serde_json::to_string(&*data).unwrap_or_default();
self.observe(Observation::ApiCall {
api: format!("chrome.storage.{area}.get"),
args: args.to_vec(),
result: Value::json(json.clone()),
});
Ok(Value::json(json))
}
"set" => {
self.observe(Observation::ApiCall {
api: format!("chrome.storage.{area}.set"),
args: args.to_vec(),
result: Value::Null,
});
if let Some(Value::Json(json, _)) = args.first()
&& let Ok(map) =
serde_json::from_str::<std::collections::HashMap<String, String>>(json)
&& let Ok(mut guard) = storage.lock()
{
guard.extend(map);
}
Ok(Value::Null)
}
"remove" | "clear" => {
self.observe(Observation::ApiCall {
api: format!("chrome.storage.{area}.{op}"),
args: args.to_vec(),
result: Value::Null,
});
if op == "clear" {
if let Ok(mut guard) = storage.lock() {
guard.clear();
}
}
Ok(Value::Null)
}
_ => Err(format!("chrome.storage.{area}.{op} is not defined")),
}
}
fn handle_runtime(&self, method: &str, args: &[Value]) -> Result<Value, String> {
match method {
"getURL" => {
let path = args.first().and_then(|v| v.as_str()).unwrap_or("");
let url = format!("chrome-extension://{}/{}", self.state.extension_id, path);
Ok(Value::string(url))
}
"sendMessage" => {
self.observe(Observation::ApiCall {
api: "chrome.runtime.sendMessage".into(),
args: args.to_vec(),
result: Value::Null,
});
if self
.profile
.is_source("chrome.runtime.sendMessage")
.is_some()
{
self.observe(Observation::ApiCall {
api: "SOURCE:chrome.runtime.sendMessage".into(),
args: args.to_vec(),
result: Value::Null,
});
}
Ok(Value::Null)
}
"getManifest" => {
let json = serde_json::to_string(&self.manifest).unwrap_or_default();
Ok(Value::json(json))
}
"id" => Ok(Value::string(self.state.extension_id.clone())),
_ => Err(format!("chrome.runtime.{method} is not defined")),
}
}
fn handle_scripting(&self, method: &str, args: &[Value]) -> Result<Value, String> {
match method {
"executeScript" | "insertCSS" | "registerContentScripts" => {
self.observe(Observation::ApiCall {
api: format!("chrome.scripting.{method}"),
args: args.to_vec(),
result: Value::Null,
});
if let Some(sink) = self.profile.is_sink(&format!("chrome.scripting.{method}")) {
self.observe(Observation::ApiCall {
api: format!("SINK:chrome.scripting.{method} [{}]", sink.cwe),
args: args.to_vec(),
result: Value::Null,
});
}
Ok(Value::Null)
}
_ => Err(format!("chrome.scripting.{method} is not defined")),
}
}
fn handle_web_request(&self, method: &str, args: &[Value]) -> Result<Value, String> {
self.observe(Observation::ApiCall {
api: format!("chrome.webRequest.{method}"),
args: args.to_vec(),
result: Value::Null,
});
Ok(Value::Null)
}
fn handle_alarms(&self, method: &str, args: &[Value]) -> Result<Value, String> {
match method {
"create" => {
self.observe(Observation::ApiCall {
api: "chrome.alarms.create".into(),
args: args.to_vec(),
result: Value::Null,
});
Ok(Value::Null)
}
"get" | "getAll" | "clear" | "clearAll" => {
let guard = self
.state
.alarms
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let json = serde_json::to_string(&*guard).unwrap_or_default();
Ok(Value::json(json))
}
_ => Err(format!("chrome.alarms.{method} is not defined")),
}
}
fn handle_permissions(&self, method: &str, _args: &[Value]) -> Result<Value, String> {
match method {
"getAll" => {
let perms = serde_json::json!({
"permissions": self.manifest.permissions,
"origins": self.manifest.host_permissions,
});
Ok(Value::json(perms.to_string()))
}
"contains" => Ok(Value::Bool(true)), "request" => Ok(Value::Bool(true)),
_ => Err(format!("chrome.permissions.{method} is not defined")),
}
}
}
impl Bridge for ChromeExtBridge {
fn call(&self, api: &str, args: &[Value]) -> Result<Value, String> {
if let Some(method) = api.strip_prefix("chrome.tabs.") {
return self.handle_tabs(method, args);
}
if let Some(method) = api.strip_prefix("chrome.cookies.") {
return self.handle_cookies(method, args);
}
if let Some(method) = api.strip_prefix("chrome.storage.") {
return self.handle_storage(method, args);
}
if let Some(method) = api.strip_prefix("chrome.runtime.") {
return self.handle_runtime(method, args);
}
if let Some(method) = api.strip_prefix("chrome.scripting.") {
return self.handle_scripting(method, args);
}
if let Some(method) = api.strip_prefix("chrome.webRequest.") {
return self.handle_web_request(method, args);
}
if let Some(method) = api.strip_prefix("chrome.alarms.") {
return self.handle_alarms(method, args);
}
if let Some(method) = api.strip_prefix("chrome.permissions.") {
return self.handle_permissions(method, args);
}
self.observe(Observation::ApiCall {
api: api.into(),
args: args.to_vec(),
result: Value::Null,
});
Err(format!("{api} is not defined"))
}
fn get_property(&self, object: &str, property: &str) -> Result<Value, String> {
match (object, property) {
("chrome.runtime", "id") => Ok(Value::string(self.state.extension_id.clone())),
("chrome.runtime", "lastError") => Ok(Value::Null),
_ => Err(format!("{object}.{property} is not defined")),
}
}
fn set_property(&self, object: &str, property: &str, value: &Value) -> Result<(), String> {
self.observe(Observation::PropertyWrite {
object: object.into(),
property: property.into(),
value: value.clone(),
});
Ok(())
}
fn provided_globals(&self) -> Vec<String> {
vec!["chrome".into()]
}
fn bootstrap_js(&self) -> String {
crate::bootstrap::generate_bootstrap(&self.manifest)
}
}
#[cfg(test)]
mod tests {
use super::*;
use jsdet_core::observation::TaintLabel;
fn make_bridge() -> ChromeExtBridge {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["tabs", "storage", "cookies"]
}"#,
)
.unwrap();
let profile = AnalysisProfile::default();
let state = ExtensionState::default();
ChromeExtBridge::new(manifest, profile, state)
}
fn make_bridge_with_profile(profile: AnalysisProfile) -> ChromeExtBridge {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["tabs", "storage", "cookies", "webRequest", "alarms"]
}"#,
)
.unwrap();
let state = ExtensionState::default();
ChromeExtBridge::new(manifest, profile, state)
}
#[test]
fn tabs_query_returns_tabs() {
let bridge = make_bridge();
let result = bridge.call("chrome.tabs.query", &[]).unwrap();
assert!(matches!(result, Value::Json(..)));
}
#[test]
fn storage_set_and_get() {
let bridge = make_bridge();
bridge
.call(
"chrome.storage.local.set",
&[Value::json(r#"{"key1": "value1"}"#)],
)
.unwrap();
let result = bridge.call("chrome.storage.local.get", &[]).unwrap();
if let Value::Json(json, _) = result {
assert!(json.contains("key1"));
}
}
#[test]
fn runtime_get_url() {
let bridge = make_bridge();
let result = bridge
.call("chrome.runtime.getURL", &[Value::string("popup.html")])
.unwrap();
if let Value::String(url, _) = result {
assert!(url.contains("popup.html"));
assert!(url.starts_with("chrome-extension://"));
}
}
#[test]
fn observations_collected() {
let bridge = make_bridge();
bridge
.call(
"chrome.tabs.create",
&[Value::json(r#"{"url":"https://evil.com"}"#)],
)
.unwrap();
bridge.call("chrome.cookies.getAll", &[]).unwrap();
let obs = bridge.take_observations();
assert!(obs.len() >= 2);
}
#[test]
fn unknown_api_returns_error() {
let bridge = make_bridge();
assert!(bridge.call("chrome.nonexistent.method", &[]).is_err());
}
#[test]
fn provided_globals() {
let bridge = make_bridge();
assert_eq!(bridge.provided_globals(), vec!["chrome"]);
}
#[test]
fn tabs_query_with_args() {
let bridge = make_bridge();
let result = bridge
.call("chrome.tabs.query", &[Value::json(r#"{"active": true}"#)])
.unwrap();
assert!(matches!(result, Value::Json(..)));
}
#[test]
fn tabs_create() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.tabs.create",
&[Value::json(r#"{"url": "https://example.com"}"#)],
)
.unwrap();
assert!(matches!(result, Value::Json(..)));
let obs = bridge.take_observations();
assert!(obs.iter().any(|o| matches!(o,
Observation::ApiCall { api, .. } if api == "chrome.tabs.create"
)));
}
#[test]
fn tabs_update() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.tabs.update",
&[
Value::string("1"),
Value::json(r#"{"url": "https://newurl.com"}"#),
],
)
.unwrap();
assert!(matches!(result, Value::Null));
}
#[test]
fn tabs_execute_script() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.tabs.executeScript",
&[Value::string("1"), Value::json(r#"{"code": "alert(1)"}"#)],
)
.unwrap();
assert!(matches!(result, Value::Null));
let obs = bridge.take_observations();
assert!(obs.iter().any(|o| matches!(o,
Observation::ApiCall { api, .. } if api == "chrome.tabs.executeScript"
)));
}
#[test]
fn tabs_send_message() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.tabs.sendMessage",
&[Value::string("1"), Value::json(r#"{"action": "test"}"#)],
)
.unwrap();
assert!(matches!(result, Value::Null));
}
#[test]
fn tabs_unknown_method() {
let bridge = make_bridge();
let result = bridge.call("chrome.tabs.nonexistent", &[]);
assert!(result.is_err());
assert!(result.unwrap_err().contains("not defined"));
}
#[test]
fn cookies_get_all() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.cookies.getAll",
&[Value::json(r#"{"domain": "example.com"}"#)],
)
.unwrap();
assert!(matches!(result, Value::Json(..)));
let obs = bridge.take_observations();
assert!(obs.iter().any(|o| matches!(o,
Observation::ApiCall { api, .. } if api == "chrome.cookies.getAll"
)));
}
#[test]
fn cookies_get() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.cookies.get",
&[Value::Json(
r#"{"url": "https://example.com", "name": "session"}"#.into(),
TaintLabel::default(),
)],
)
.unwrap();
assert!(matches!(result, Value::Json(..)));
let obs = bridge.take_observations();
assert!(obs.iter().any(|o| matches!(o,
Observation::ApiCall { api, .. } if api == "chrome.cookies.get"
)));
}
#[test]
fn cookies_set() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.cookies.set",
&[Value::Json(
r#"{"url": "https://example.com", "name": "new", "value": "value"}"#.into(),
TaintLabel::default(),
)],
)
.unwrap();
assert!(matches!(result, Value::Null));
let obs = bridge.take_observations();
assert!(obs.iter().any(|o| matches!(o,
Observation::ApiCall { api, .. } if api == "chrome.cookies.set"
)));
}
#[test]
fn cookies_remove() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.cookies.remove",
&[Value::Json(
r#"{"url": "https://example.com", "name": "session"}"#.into(),
TaintLabel::default(),
)],
)
.unwrap();
assert!(matches!(result, Value::Null));
let obs = bridge.take_observations();
assert!(obs.iter().any(|o| matches!(o,
Observation::ApiCall { api, .. } if api == "chrome.cookies.remove"
)));
}
#[test]
fn cookies_unknown_method() {
let bridge = make_bridge();
let result = bridge.call("chrome.cookies.nonexistent", &[]);
assert!(result.is_err());
}
#[test]
fn storage_local_get_empty() {
let bridge = make_bridge();
let result = bridge.call("chrome.storage.local.get", &[]).unwrap();
assert!(matches!(result, Value::Json(..)));
}
#[test]
fn storage_local_get_with_keys() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.storage.local.get",
&[Value::json(r#"["key1", "key2"]"#)],
)
.unwrap();
assert!(matches!(result, Value::Json(..)));
}
#[test]
fn storage_local_set_and_get_roundtrip() {
let bridge = make_bridge();
bridge
.call(
"chrome.storage.local.set",
&[Value::json(r#"{"testkey": "testvalue"}"#)],
)
.unwrap();
let result = bridge.call("chrome.storage.local.get", &[]).unwrap();
if let Value::Json(json, _) = result {
assert!(json.contains("testkey"));
assert!(json.contains("testvalue"));
}
}
#[test]
fn storage_local_remove() {
let bridge = make_bridge();
let result = bridge
.call("chrome.storage.local.remove", &[Value::json(r#""key1""#)])
.unwrap();
assert!(matches!(result, Value::Null));
}
#[test]
fn storage_local_clear() {
let bridge = make_bridge();
bridge
.call(
"chrome.storage.local.set",
&[Value::json(r#"{"temp": "value"}"#)],
)
.unwrap();
let result = bridge.call("chrome.storage.local.clear", &[]).unwrap();
assert!(matches!(result, Value::Null));
let get_result = bridge.call("chrome.storage.local.get", &[]).unwrap();
if let Value::Json(json, _) = get_result {
assert_eq!(json, "{}");
}
}
#[test]
fn storage_sync_get() {
let bridge = make_bridge();
let result = bridge.call("chrome.storage.sync.get", &[]).unwrap();
assert!(matches!(result, Value::Json(..)));
}
#[test]
fn storage_sync_set() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.storage.sync.set",
&[Value::json(r#"{"synckey": "syncvalue"}"#)],
)
.unwrap();
assert!(matches!(result, Value::Null));
}
#[test]
fn storage_sync_remove() {
let bridge = make_bridge();
let result = bridge
.call("chrome.storage.sync.remove", &[Value::json(r#""key""#)])
.unwrap();
assert!(matches!(result, Value::Null));
}
#[test]
fn storage_sync_clear() {
let bridge = make_bridge();
let result = bridge.call("chrome.storage.sync.clear", &[]).unwrap();
assert!(matches!(result, Value::Null));
}
#[test]
fn storage_shortcut_local_get() {
let bridge = make_bridge();
let result = bridge.call("chrome.storage.get", &[]).unwrap();
assert!(matches!(result, Value::Json(..)));
}
#[test]
fn storage_unknown_area() {
let bridge = make_bridge();
let result = bridge.call("chrome.storage.unknown.get", &[]);
assert!(result.is_ok() || result.is_err());
}
#[test]
fn runtime_get_url_empty_path() {
let bridge = make_bridge();
let result = bridge.call("chrome.runtime.getURL", &[]).unwrap();
if let Value::String(url, _) = result {
assert!(url.starts_with("chrome-extension://"));
assert!(url.ends_with("/"));
}
}
#[test]
fn runtime_get_url_with_path() {
let bridge = make_bridge();
let result = bridge
.call("chrome.runtime.getURL", &[Value::string("js/content.js")])
.unwrap();
if let Value::String(url, _) = result {
assert!(url.contains("js/content.js"));
}
}
#[test]
fn runtime_send_message() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.runtime.sendMessage",
&[Value::json(r#"{"action": "test"}"#)],
)
.unwrap();
assert!(matches!(result, Value::Null));
let obs = bridge.take_observations();
assert!(obs.iter().any(|o| matches!(o,
Observation::ApiCall { api, .. } if api == "chrome.runtime.sendMessage"
)));
}
#[test]
fn runtime_get_manifest() {
let bridge = make_bridge();
let result = bridge.call("chrome.runtime.getManifest", &[]).unwrap();
if let Value::Json(json, _) = result {
assert!(json.contains("Test"));
assert!(json.contains("manifest_version"));
}
}
#[test]
fn runtime_id() {
let bridge = make_bridge();
let result = bridge.call("chrome.runtime.id", &[]).unwrap();
if let Value::String(id, _) = result {
assert!(!id.is_empty());
}
}
#[test]
fn runtime_unknown_method() {
let bridge = make_bridge();
let result = bridge.call("chrome.runtime.nonexistent", &[]);
assert!(result.is_err());
}
#[test]
fn scripting_execute_script() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.scripting.executeScript",
&[Value::Json(
r#"{"target": {"tabId": 1}, "func": "() => {}"}"#.into(),
TaintLabel::default(),
)],
)
.unwrap();
assert!(matches!(result, Value::Null));
let obs = bridge.take_observations();
assert!(obs.iter().any(|o| matches!(o,
Observation::ApiCall { api, .. } if api == "chrome.scripting.executeScript"
)));
}
#[test]
fn scripting_insert_css() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.scripting.insertCSS",
&[Value::Json(
r#"{"target": {"tabId": 1}, "css": "body{}"}"#.into(),
TaintLabel::default(),
)],
)
.unwrap();
assert!(matches!(result, Value::Null));
let obs = bridge.take_observations();
assert!(obs.iter().any(|o| matches!(o,
Observation::ApiCall { api, .. } if api == "chrome.scripting.insertCSS"
)));
}
#[test]
fn scripting_register_content_scripts() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.scripting.registerContentScripts",
&[Value::Json(
r#"[{"id": "script1", "matches": ["<all_urls>"], "js": ["content.js"]}]"#
.into(),
TaintLabel::default(),
)],
)
.unwrap();
assert!(matches!(result, Value::Null));
}
#[test]
fn scripting_unknown_method() {
let bridge = make_bridge();
let result = bridge.call("chrome.scripting.nonexistent", &[]);
assert!(result.is_err());
}
#[test]
fn webrequest_on_before_request() {
let bridge = make_bridge();
let result = bridge
.call("chrome.webRequest.onBeforeRequest.addListener", &[])
.unwrap();
assert!(matches!(result, Value::Null));
let obs = bridge.take_observations();
assert!(obs.iter().any(|o| matches!(o,
Observation::ApiCall { api, .. } if api == "chrome.webRequest.onBeforeRequest.addListener"
)));
}
#[test]
fn webrequest_on_before_send_headers() {
let bridge = make_bridge();
let result = bridge
.call("chrome.webRequest.onBeforeSendHeaders.addListener", &[])
.unwrap();
assert!(matches!(result, Value::Null));
}
#[test]
fn webrequest_any_method() {
let bridge = make_bridge();
let result = bridge
.call("chrome.webRequest.onCompleted.addListener", &[])
.unwrap();
assert!(matches!(result, Value::Null));
let result = bridge
.call("chrome.webRequest.onErrorOccurred.addListener", &[])
.unwrap();
assert!(matches!(result, Value::Null));
}
#[test]
fn alarms_create() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.alarms.create",
&[
Value::string("alarm1"),
Value::json(r#"{"delayInMinutes": 5}"#),
],
)
.unwrap();
assert!(matches!(result, Value::Null));
let obs = bridge.take_observations();
assert!(obs.iter().any(|o| matches!(o,
Observation::ApiCall { api, .. } if api == "chrome.alarms.create"
)));
}
#[test]
fn alarms_get() {
let bridge = make_bridge();
let result = bridge
.call("chrome.alarms.get", &[Value::string("alarm1")])
.unwrap();
assert!(matches!(result, Value::Json(..)));
}
#[test]
fn alarms_get_all() {
let bridge = make_bridge();
let result = bridge.call("chrome.alarms.getAll", &[]).unwrap();
assert!(matches!(result, Value::Json(..)));
}
#[test]
fn alarms_clear() {
let bridge = make_bridge();
let result = bridge
.call("chrome.alarms.clear", &[Value::string("alarm1")])
.unwrap();
assert!(matches!(result, Value::Json(..)));
}
#[test]
fn alarms_clear_all() {
let bridge = make_bridge();
let result = bridge.call("chrome.alarms.clearAll", &[]).unwrap();
assert!(matches!(result, Value::Json(..)));
}
#[test]
fn alarms_unknown_method() {
let bridge = make_bridge();
let result = bridge.call("chrome.alarms.nonexistent", &[]);
assert!(result.is_err());
}
#[test]
fn permissions_get_all() {
let bridge = make_bridge();
let result = bridge.call("chrome.permissions.getAll", &[]).unwrap();
if let Value::Json(json, _) = result {
assert!(json.contains("permissions"));
assert!(json.contains("tabs")); }
}
#[test]
fn permissions_contains() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.permissions.contains",
&[Value::json(r#"{"permissions": ["tabs"]}"#)],
)
.unwrap();
if let Value::Bool(has_perm) = result {
assert!(has_perm); }
}
#[test]
fn permissions_request() {
let bridge = make_bridge();
let result = bridge
.call(
"chrome.permissions.request",
&[Value::json(r#"{"permissions": ["history"]}"#)],
)
.unwrap();
if let Value::Bool(granted) = result {
assert!(granted); }
}
#[test]
fn permissions_unknown_method() {
let bridge = make_bridge();
let result = bridge.call("chrome.permissions.nonexistent", &[]);
assert!(result.is_err());
}
#[test]
fn eval_with_csp_allowing() {
let manifest = Manifest::parse(
r#"{
"name": "Permissive",
"manifest_version": 3,
"version": "1.0",
"content_security_policy": {
"extension_pages": "script-src 'self' 'unsafe-eval'"
}
}"#,
)
.unwrap();
let bridge = ChromeExtBridge::new(
manifest,
AnalysisProfile::default(),
ExtensionState::default(),
);
let result = bridge.call("eval", &[Value::string("1+1")]);
if let Err(e) = result {
assert!(!e.contains("unsafe-eval"));
}
let obs = bridge.take_observations();
assert!(
obs.iter()
.any(|o| matches!(o, Observation::DynamicCodeExec { .. }))
);
}
#[test]
fn function_constructor_blocked_by_csp() {
let manifest = Manifest::parse(
r#"{
"name": "Strict",
"manifest_version": 3,
"version": "1.0",
"content_security_policy": {
"extension_pages": "script-src 'self'"
}
}"#,
)
.unwrap();
let bridge = ChromeExtBridge::new(
manifest,
AnalysisProfile::default(),
ExtensionState::default(),
);
let result = bridge.call("Function", &[Value::string("return 1")]);
assert!(result.is_err());
assert!(result.unwrap_err().contains("unsafe-eval"));
}
#[test]
fn function_constructor_allowed() {
let bridge = make_bridge();
let _result = bridge.call("Function", &[Value::string("return 1")]);
let obs = bridge.take_observations();
assert!(
obs.iter()
.any(|o| matches!(o, Observation::DynamicCodeExec { .. }))
);
}
#[test]
fn eval_code_preview_truncated() {
let bridge = make_bridge();
let long_code = "x".repeat(1000);
let _result = bridge.call(
"eval",
&[Value::String(long_code.clone(), TaintLabel::default())],
);
let obs = bridge.take_observations();
let found = obs
.iter()
.any(|o| matches!(o, Observation::DynamicCodeExec { .. }));
assert!(found);
let preview = obs.iter().find_map(|o| {
if let Observation::DynamicCodeExec { code_preview, .. } = o {
Some(code_preview.clone())
} else {
None
}
});
assert!(preview.is_some());
}
#[test]
fn take_observations_drains_list() {
let bridge = make_bridge();
bridge.call("chrome.tabs.query", &[]).unwrap();
bridge.call("chrome.cookies.getAll", &[]).unwrap();
let obs1 = bridge.take_observations();
assert!(!obs1.is_empty());
let obs2 = bridge.take_observations();
assert!(obs2.is_empty());
}
#[test]
fn take_observations_returns_correct_count() {
let bridge = make_bridge();
bridge.call("chrome.tabs.create", &[]).unwrap();
bridge.call("chrome.tabs.update", &[]).unwrap();
bridge.call("chrome.tabs.sendMessage", &[]).unwrap();
let obs = bridge.take_observations();
assert!(obs.len() >= 3);
}
#[test]
fn observations_include_args() {
let bridge = make_bridge();
bridge
.call(
"chrome.tabs.create",
&[Value::json(r#"{"url": "https://example.com"}"#)],
)
.unwrap();
let obs = bridge.take_observations();
let found = obs.iter().any(|o| {
if let Observation::ApiCall { api, args, .. } = o {
api == "chrome.tabs.create" && !args.is_empty()
} else {
false
}
});
assert!(found);
}
#[test]
fn get_property_runtime_id() {
let bridge = make_bridge();
let result = bridge.get_property("chrome.runtime", "id").unwrap();
if let Value::String(id, _) = result {
assert!(!id.is_empty());
}
}
#[test]
fn get_property_runtime_last_error() {
let bridge = make_bridge();
let result = bridge.get_property("chrome.runtime", "lastError").unwrap();
assert!(matches!(result, Value::Null));
}
#[test]
fn get_property_unknown() {
let bridge = make_bridge();
let result = bridge.get_property("chrome.unknown", "property");
assert!(result.is_err());
}
#[test]
fn get_property_unknown_property() {
let bridge = make_bridge();
let result = bridge.get_property("chrome.runtime", "unknownProperty");
assert!(result.is_err());
}
#[test]
fn set_property_records_observation() {
let bridge = make_bridge();
bridge
.set_property("chrome.storage.local", "myKey", &Value::string("myValue"))
.unwrap();
let obs = bridge.take_observations();
let found = obs.iter().any(|o| {
matches!(o,
Observation::PropertyWrite { object, property, .. }
if object == "chrome.storage.local" && property == "myKey"
)
});
assert!(found);
}
#[test]
fn set_property_any_object() {
let bridge = make_bridge();
let result = bridge.set_property("some.object", "prop", &Value::Null);
assert!(result.is_ok());
}
#[test]
fn bootstrap_js_returns_non_empty() {
let bridge = make_bridge();
let js = bridge.bootstrap_js();
assert!(!js.is_empty());
assert!(js.len() > 100);
}
#[test]
fn bootstrap_js_contains_chrome() {
let bridge = make_bridge();
let js = bridge.bootstrap_js();
assert!(js.contains("chrome"));
}
#[test]
fn unknown_chrome_namespace() {
let bridge = make_bridge();
let result = bridge.call("chrome.totallyunknown.something", &[]);
assert!(result.is_err());
assert!(result.unwrap_err().contains("not defined"));
}
#[test]
fn unknown_top_level() {
let bridge = make_bridge();
let result = bridge.call("notchrome.something", &[]);
assert!(result.is_err());
}
#[test]
fn empty_api_name() {
let bridge = make_bridge();
let result = bridge.call("", &[]);
assert!(result.is_err());
}
#[test]
fn sink_detection_tabs_update() {
let profile = AnalysisProfile::parse(
r#"
[[sinks]]
api = "chrome.tabs.update"
dangerous_arg = 1
cwe = "CWE-601"
severity = "high"
"#,
)
.unwrap();
let bridge = make_bridge_with_profile(profile);
bridge
.call(
"chrome.tabs.update",
&[
Value::string("1"),
Value::json(r#"{"url": "https://evil.com"}"#),
],
)
.unwrap();
let obs = bridge.take_observations();
let found = obs.iter().any(|o| {
matches!(o,
Observation::ApiCall { api, .. } if api.contains("SINK:")
)
});
assert!(found);
}
#[test]
fn sink_detection_tabs_execute_script() {
let profile = AnalysisProfile::parse(
r#"
[[sinks]]
api = "chrome.tabs.executeScript"
cwe = "CWE-94"
"#,
)
.unwrap();
let bridge = make_bridge_with_profile(profile);
bridge
.call(
"chrome.tabs.executeScript",
&[Value::string("1"), Value::json(r#"{"code": "alert(1)"}"#)],
)
.unwrap();
let obs = bridge.take_observations();
let found = obs.iter().any(|o| {
matches!(o,
Observation::ApiCall { api, .. } if api.contains("SINK:") && api.contains("CWE-94")
)
});
assert!(found);
}
#[test]
fn sink_detection_scripting_execute_script() {
let profile = AnalysisProfile::parse(
r#"
[[sinks]]
api = "chrome.scripting.executeScript"
cwe = "CWE-94"
"#,
)
.unwrap();
let bridge = make_bridge_with_profile(profile);
bridge.call("chrome.scripting.executeScript", &[]).unwrap();
let obs = bridge.take_observations();
let found = obs.iter().any(|o| {
matches!(o,
Observation::ApiCall { api, .. } if api.contains("SINK:")
)
});
assert!(found);
}
#[test]
fn sink_detection_scripting_insert_css() {
let profile = AnalysisProfile::parse(
r#"
[[sinks]]
api = "chrome.scripting.insertCSS"
cwe = "CWE-79"
"#,
)
.unwrap();
let bridge = make_bridge_with_profile(profile);
bridge.call("chrome.scripting.insertCSS", &[]).unwrap();
let obs = bridge.take_observations();
let found = obs.iter().any(|o| {
matches!(o,
Observation::ApiCall { api, .. } if api.contains("SINK:")
)
});
assert!(found);
}
#[test]
fn source_detection_send_message() {
let profile = AnalysisProfile::parse(
r#"
[[sources]]
api = "chrome.runtime.sendMessage"
taint_id = 1
"#,
)
.unwrap();
let bridge = make_bridge_with_profile(profile);
bridge
.call("chrome.runtime.sendMessage", &[Value::Null])
.unwrap();
let obs = bridge.take_observations();
let found = obs.iter().any(|o| {
matches!(o,
Observation::ApiCall { api, .. } if api.contains("SOURCE:")
)
});
assert!(found);
}
#[test]
fn no_sink_detection_for_unconfigured_api() {
let profile = AnalysisProfile::default();
let bridge = make_bridge_with_profile(profile);
bridge.call("chrome.tabs.update", &[]).unwrap();
let obs = bridge.take_observations();
let found = obs.iter().any(|o| {
matches!(o,
Observation::ApiCall { api, .. } if api.contains("SINK:")
)
});
assert!(!found);
}
#[test]
fn wasm_sandbox_detects_eval_via_bridge() {
use jsdet_core::bridge::Bridge;
use jsdet_core::{CompiledModule, PersistentSandbox, SandboxConfig};
use std::sync::Arc;
let module = CompiledModule::new().unwrap();
let manifest = Manifest::parse(r#"{"name":"T","manifest_version":3,"version":"1.0","background":{"service_worker":"sw.js"}}"#).unwrap();
let profile = AnalysisProfile::default();
let state = crate::state::ExtensionState::default_with_id("test");
let bridge: Arc<dyn Bridge> =
Arc::new(ChromeExtBridge::new(manifest.clone(), profile, state));
let config = SandboxConfig::default();
let mut sb = PersistentSandbox::new(&module, bridge, &config).unwrap();
let bootstrap = crate::bootstrap::generate_bootstrap(&manifest);
let handler = r#"
chrome.runtime.onMessage.addListener(function(msg) {
eval(msg.code);
});
"#
.to_string();
sb.load(&[bootstrap, handler]).unwrap();
let obs = sb.eval_only(
r#"
chrome.runtime._fireOnMessage(
{code: "alert(SLN_TEST_123)"},
{tab: {id: 1}, id: "test"},
function() {}
);
"#,
);
eprintln!("Observations ({}):", obs.len());
for o in &obs {
eprintln!(" {:?}", o);
}
let has_eval = obs.iter().any(|o| match o {
jsdet_core::Observation::ApiCall { api, .. } => api == "eval",
_ => false,
});
assert!(
has_eval,
"should detect eval() call. Got {} observations",
obs.len()
);
}
}