Skip to content

Commit 024a3bf

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. The TOML configuration file values are now used only as an initial seed for the database during the first run. On subsequent runs, any values changed and persisted via the Web UI will take precedence over the config file defaults. Fixed a bug in the web-ui store where changing one setting would cause all other settings to be re-saved with their initial client-side values, unintentionally overwriting database settings with stale defaults. Dependent on: ActivityWatch/aw-webui#795
1 parent 9a8802a commit 024a3bf

File tree

6 files changed

+86
-13
lines changed

6 files changed

+86
-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: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,55 @@ 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: &str| -> Vec<String> {
141+
serde_json::from_str::<String>(raw)
142+
.unwrap_or_else(|_| raw.trim_matches('"').to_string())
143+
.split(',')
144+
.map(|s| s.trim().to_string())
145+
.filter(|s| !s.is_empty())
146+
.collect()
147+
};
148+
let parse_bool = |raw: &str| -> bool {
149+
serde_json::from_str::<bool>(raw).unwrap_or_else(|_| raw.trim_matches('"') == "true")
150+
};
151+
// Sync settings between Config file and Database.
152+
// On the first run (when a key is missing in the DB), we seed the DB with the value from the config file.
153+
// On subsequent runs, we always prefer the DB value (which might have been changed via the UI).
154+
let sync =
155+
|key: &str, current_val: &mut String, to_save: String| match db.get_key_value(key) {
156+
Ok(raw) => *current_val = raw,
157+
Err(_) => {
158+
db.set_key_value(key, &to_save).ok();
159+
*current_val = to_save;
160+
}
161+
};
162+
163+
let mut raw_cors = String::new();
164+
sync("settings.cors", &mut raw_cors, serde_json::to_string(&config.cors.join(",")).unwrap());
165+
config.cors = parse_cors_list(&raw_cors);
166+
167+
let mut raw_cors_regex = String::new();
168+
sync("settings.cors_regex", &mut raw_cors_regex, serde_json::to_string(&config.cors_regex.join(",")).unwrap());
169+
config.cors_regex = parse_cors_list(&raw_cors_regex);
170+
171+
let mut raw_chrome = String::new();
172+
sync("settings.cors_allow_aw_chrome_extension", &mut raw_chrome, serde_json::to_string(&config.cors_allow_aw_chrome_extension).unwrap());
173+
config.cors_allow_aw_chrome_extension = parse_bool(&raw_chrome);
174+
175+
let mut raw_mozilla = String::new();
176+
sync("settings.cors_allow_all_mozilla_extension", &mut raw_mozilla, serde_json::to_string(&config.cors_allow_all_mozilla_extension).unwrap());
177+
config.cors_allow_all_mozilla_extension = parse_bool(&raw_mozilla);
178+
}
135179
let cors = cors::cors(&config);
136180
let hostcheck = hostcheck::HostCheck::new(&config);
137181
let custom_static = config.custom_static.clone();

0 commit comments

Comments
 (0)