Skip to content

Commit 102e1f1

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 by default. This commit removes that blanket permission and introduces granular control through both static configuration and the Web UI. We've added 2 new fields to the file configuration (cors_allow_aw_chrome_extension and cors_allow_all_mozilla_extension) and 4 new settings to the Web UI (Fixed origins, Regex origins, and extension-specific shortcuts). The server now merges these settings to determine the final set of authorized origins, ensuring a more secure and flexible configuration. Dependent on: ActivityWatch/aw-webui#795
1 parent 9a8802a commit 102e1f1

File tree

6 files changed

+81
-13
lines changed

6 files changed

+81
-13
lines changed

.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

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,22 @@ Available options:
6060

6161
# Additional regex CORS origins to allow (e.g. for sideloaded browser extensions)
6262
#cors_regex = ["chrome-extension://yourextensionidhere"]
63+
64+
# Allow official ActivityWatch Chrome extension? (default: true)
65+
#cors_allow_aw_chrome_extension = true
66+
67+
# Allow all Firefox extensions? (default: false, DANGEROUS)
68+
#cors_allow_all_mozilla_extension = false
6369
```
6470

6571
#### Custom CORS Origins
6672

6773
By default, the server allows requests from:
6874
- The server's own origin (`http://127.0.0.1:<port>`, `http://localhost:<port>`)
69-
- The official Chrome extension (`chrome-extension://nglaklhklhcoonedhgnpgddginnjdadi`)
70-
- All Firefox extensions (`moz-extension://.*`)
75+
- The official Chrome extension (`chrome-extension://nglaklhklhcoonedhgnpgddginnjdadi`) if `cors_allow_aw_chrome_extension` is true (default).
76+
- All Firefox extensions (`moz-extension://.*`) ONLY IF `cors_allow_all_mozilla_extension` is set to true.
7177

72-
To allow additional origins (e.g. a sideloaded Chrome extension), add them to your config:
78+
To allow additional origins (e.g. a sideloaded Chrome extension), add them to your `cors` or `cors_regex` config:
7379

7480
```toml
7581
# Allow a specific sideloaded Chrome extension

aw-server/src/config.rs

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

39+
#[serde(default = "default_true")]
40+
pub cors_allow_aw_chrome_extension: bool,
41+
42+
#[serde(default = "default_false")]
43+
pub cors_allow_all_mozilla_extension: bool,
44+
3945
// A mapping of watcher names to paths where the
4046
// custom visualizations are located.
4147
#[serde(default = "default_custom_static")]
@@ -50,6 +56,8 @@ impl Default for AWConfig {
5056
testing: default_testing(),
5157
cors: default_cors(),
5258
cors_regex: default_cors(),
59+
cors_allow_aw_chrome_extension: default_true(),
60+
cors_allow_all_mozilla_extension: default_false(),
5361
custom_static: default_custom_static(),
5462
}
5563
}
@@ -91,6 +99,14 @@ fn default_testing() -> bool {
9199
is_testing()
92100
}
93101

102+
fn default_true() -> bool {
103+
true
104+
}
105+
106+
fn default_false() -> bool {
107+
false
108+
}
109+
94110
fn default_port() -> u16 {
95111
if is_testing() {
96112
5666

aw-server/src/endpoints/cors.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,23 @@ pub fn cors(config: &AWConfig) -> rocket_cors::Cors {
99
let mut allowed_exact_origins = vec![root_url, root_url_localhost];
1010
allowed_exact_origins.extend(config.cors.clone());
1111

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());
12+
let mut allowed_regex_origins = config.cors_regex.clone();
13+
14+
if config.cors_allow_aw_chrome_extension {
15+
allowed_regex_origins.push("chrome-extension://nglaklhklhcoonedhgnpgddginnjdadi".to_string());
1516
}
16-
let mut allowed_regex_origins = vec![
17-
"chrome-extension://nglaklhklhcoonedhgnpgddginnjdadi".to_string(),
17+
18+
if config.cors_allow_all_mozilla_extension {
1819
// Every version of a mozilla extension has its own ID to avoid fingerprinting, so we
1920
// 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());
21+
allowed_regex_origins.push("moz-extension://.*".to_string());
22+
}
23+
2324
if config.testing {
25+
allowed_exact_origins.extend(vec![
26+
"http://127.0.0.1:27180".to_string(),
27+
"http://localhost:27180".to_string(),
28+
]);
2429
allowed_regex_origins.push("chrome-extension://.*".to_string());
2530
}
2631

aw-server/src/endpoints/mod.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,50 @@ 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(
131+
server_state: ServerState,
132+
mut config: AWConfig,
133+
) -> rocket::Rocket<rocket::Build> {
131134
info!(
132135
"Starting aw-server-rust at {}:{}",
133136
config.address, config.port
134137
);
138+
{
139+
let db = server_state.datastore.lock().unwrap();
140+
let parse_cors_list = |raw: String| -> Vec<String> {
141+
serde_json::from_str::<String>(&raw)
142+
.unwrap_or_default()
143+
.split(',')
144+
.map(|s| s.trim().to_string())
145+
.filter(|s| !s.is_empty())
146+
.collect()
147+
};
148+
let parse_bool = |raw: String| -> bool {
149+
serde_json::from_str::<bool>(&raw)
150+
.ok()
151+
.or_else(|| {
152+
if raw == "true" {
153+
Some(true)
154+
} else {
155+
Some(false)
156+
}
157+
})
158+
.unwrap()
159+
};
160+
161+
if let Ok(raw) = db.get_key_value("settings.cors") {
162+
config.cors.extend(parse_cors_list(raw));
163+
}
164+
if let Ok(raw) = db.get_key_value("settings.cors_regex") {
165+
config.cors_regex.extend(parse_cors_list(raw));
166+
}
167+
if let Ok(raw) = db.get_key_value("settings.cors_allow_aw_chrome_extension") {
168+
config.cors_allow_aw_chrome_extension |= parse_bool(raw);
169+
}
170+
if let Ok(raw) = db.get_key_value("settings.cors_allow_all_mozilla_extension") {
171+
config.cors_allow_all_mozilla_extension |= parse_bool(raw);
172+
}
173+
}
135174
let cors = cors::cors(&config);
136175
let hostcheck = hostcheck::HostCheck::new(&config);
137176
let custom_static = config.custom_static.clone();

aw-webui

0 commit comments

Comments
 (0)