Skip to content

Commit 61a534b

Browse files
committed
[SEC] restrict CORS to authorized extension IDs
Fixes a security issue where any Firefox extension (moz-extension://.*) could access the ActivityWatch server without any restriction. Previously, the CORS configuration included a wildcard for all Mozilla extensions. This commit removes that blanket permission and introduces logic to parse specific authorized extension IDs from the server settings. Since manually adding extension URIs can be technical for non-developers, we've consolidated the configuration into a single 'Cors' field in the settings UI. The server automatically sorts these into exact matches (for http/https) or regex matches (for browser extensions), ensuring a simpler and more secure configuration experience. Dependent on: odoo/aw-webui#1
1 parent 9a8802a commit 61a534b

7 files changed

Lines changed: 95 additions & 18 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ NDK
1010
*.sqlite*
1111
*.db
1212
*.db-journal
13+
14+
.vscode

Cargo.lock

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aw-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ aw-datastore = { path = "../aw-datastore" }
3434
aw-models = { path = "../aw-models" }
3535
aw-transform = { path = "../aw-transform" }
3636
aw-query = { path = "../aw-query" }
37+
regex = "1.12.3"
3738

3839
[target.'cfg(target_os="linux")'.dependencies]
3940
sd-notify = "0.4.2"

aw-server/src/config.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ pub struct AWConfig {
3636
#[serde(default = "default_cors")]
3737
pub cors_regex: Vec<String>,
3838

39+
#[serde(default = "default_true")]
40+
pub allow_aw_chrome_extension: bool,
41+
42+
#[serde(default = "default_false")]
43+
pub allow_all_mozilla_extension: bool,
44+
45+
#[serde(default = "default_cors")]
46+
pub cors_from_settings: Vec<String>,
47+
48+
#[serde(default = "default_cors")]
49+
pub cors_regex_from_settings: Vec<String>,
50+
51+
#[serde(skip)]
52+
pub allow_aw_chrome_extension_from_settings: Option<bool>,
53+
54+
#[serde(skip)]
55+
pub allow_all_mozilla_extension_from_settings: Option<bool>,
56+
3957
// A mapping of watcher names to paths where the
4058
// custom visualizations are located.
4159
#[serde(default = "default_custom_static")]
@@ -50,6 +68,12 @@ impl Default for AWConfig {
5068
testing: default_testing(),
5169
cors: default_cors(),
5270
cors_regex: default_cors(),
71+
allow_aw_chrome_extension: default_true(),
72+
allow_all_mozilla_extension: default_false(),
73+
cors_from_settings: default_cors(),
74+
cors_regex_from_settings: default_cors(),
75+
allow_aw_chrome_extension_from_settings: None,
76+
allow_all_mozilla_extension_from_settings: None,
5377
custom_static: default_custom_static(),
5478
}
5579
}
@@ -91,6 +115,14 @@ fn default_testing() -> bool {
91115
is_testing()
92116
}
93117

118+
fn default_true() -> bool {
119+
true
120+
}
121+
122+
fn default_false() -> bool {
123+
false
124+
}
125+
94126
fn default_port() -> u16 {
95127
if is_testing() {
96128
5666

aw-server/src/endpoints/cors.rs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,29 @@ pub fn cors(config: &AWConfig) -> rocket_cors::Cors {
88
let root_url_localhost = format!("http://localhost:{}", config.port);
99
let mut allowed_exact_origins = vec![root_url, root_url_localhost];
1010
allowed_exact_origins.extend(config.cors.clone());
11+
allowed_exact_origins.extend(config.cors_from_settings.clone());
1112

12-
if config.testing {
13-
allowed_exact_origins.push("http://127.0.0.1:27180".to_string());
14-
allowed_exact_origins.push("http://localhost:27180".to_string());
13+
let mut allowed_regex_origins = config.cors_regex.clone();
14+
allowed_regex_origins.extend(config.cors_regex_from_settings.clone());
15+
16+
// Settings-based flags override file-based config flags
17+
let allow_chrome = config.allow_aw_chrome_extension_from_settings
18+
.unwrap_or(config.allow_aw_chrome_extension);
19+
if allow_chrome {
20+
allowed_regex_origins.push("chrome-extension://nglaklhklhcoonedhgnpgddginnjdadi".to_string());
1521
}
16-
let mut allowed_regex_origins = vec![
17-
"chrome-extension://nglaklhklhcoonedhgnpgddginnjdadi".to_string(),
18-
// Every version of a mozilla extension has its own ID to avoid fingerprinting, so we
19-
// unfortunately have to allow all extensions to have access to aw-server
20-
"moz-extension://.*".to_string(),
21-
];
22-
allowed_regex_origins.extend(config.cors_regex.clone());
22+
23+
let allow_mozilla = config.allow_all_mozilla_extension_from_settings
24+
.unwrap_or(config.allow_all_mozilla_extension);
25+
if allow_mozilla {
26+
allowed_regex_origins.push("moz-extension://.*".to_string());
27+
}
28+
2329
if config.testing {
24-
allowed_regex_origins.push("chrome-extension://.*".to_string());
30+
allowed_exact_origins.extend(vec![
31+
"http://127.0.0.1:27180".to_string(),
32+
"http://localhost:27180".to_string(),
33+
]);
2534
}
2635

2736
let allowed_origins = AllowedOrigins::some(&allowed_exact_origins, &allowed_regex_origins);
@@ -31,7 +40,6 @@ pub fn cors(config: &AWConfig) -> rocket_cors::Cors {
3140
.collect();
3241
let allowed_headers = AllowedHeaders::all(); // TODO: is this unsafe?
3342

34-
// You can also deserialize this
3543
rocket_cors::CorsOptions {
3644
allowed_origins,
3745
allowed_methods,

aw-server/src/endpoints/mod.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,44 @@ fn get_file(file: PathBuf, state: &State<ServerState>) -> Option<(ContentType, V
127127
Some((content_type, asset))
128128
}
129129

130-
pub fn build_rocket(server_state: ServerState, config: AWConfig) -> rocket::Rocket<rocket::Build> {
130+
pub fn build_rocket(server_state: ServerState, mut config: AWConfig) -> rocket::Rocket<rocket::Build> {
131131
info!(
132132
"Starting aw-server-rust at {}:{}",
133133
config.address, config.port
134134
);
135+
{
136+
let db = server_state.datastore.lock().unwrap();
137+
let parse_cors_list = |raw: String| -> Vec<String> {
138+
serde_json::from_str::<String>(&raw)
139+
.unwrap_or_default()
140+
.split(',')
141+
.map(|s| s.trim().to_string())
142+
.filter(|s| !s.is_empty())
143+
.collect()
144+
};
145+
let parse_bool = |raw: String| -> Option<bool> {
146+
serde_json::from_str::<bool>(&raw)
147+
.ok()
148+
.or_else(|| {
149+
if raw == "true" { Some(true) }
150+
else if raw == "false" { Some(false) }
151+
else { None }
152+
})
153+
};
154+
155+
if let Ok(raw) = db.get_key_value("settings.cors") {
156+
config.cors_from_settings = parse_cors_list(raw);
157+
}
158+
if let Ok(raw) = db.get_key_value("settings.cors_regex") {
159+
config.cors_regex_from_settings = parse_cors_list(raw);
160+
}
161+
if let Ok(raw) = db.get_key_value("settings.allow_aw_chrome_extension") {
162+
config.allow_aw_chrome_extension_from_settings = parse_bool(raw);
163+
}
164+
if let Ok(raw) = db.get_key_value("settings.allow_all_mozilla_extension") {
165+
config.allow_all_mozilla_extension_from_settings = parse_bool(raw);
166+
}
167+
} // lock released here
135168
let cors = cors::cors(&config);
136169
let hostcheck = hostcheck::HostCheck::new(&config);
137170
let custom_static = config.custom_static.clone();

0 commit comments

Comments
 (0)