From 268528b6610afcb8255d4652c089ac2f25366242 Mon Sep 17 00:00:00 2001 From: "Abderraouf Ghrissi (abgh)" Date: Thu, 9 Apr 2026 16:21:13 +0200 Subject: [PATCH] [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 (allow_aw_chrome_extension and 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: https://github.com/ActivityWatch/aw-server-rust/pull/581 edited according to the last changes --- src/components/CorsConfigModal.vue | 107 +++++++++++++++++++++++++++++ src/stores/cors.ts | 67 ++++++++++++++++++ src/views/settings/Settings.vue | 7 ++ 3 files changed, 181 insertions(+) create mode 100644 src/components/CorsConfigModal.vue create mode 100644 src/stores/cors.ts diff --git a/src/components/CorsConfigModal.vue b/src/components/CorsConfigModal.vue new file mode 100644 index 00000000..bb93ac6b --- /dev/null +++ b/src/components/CorsConfigModal.vue @@ -0,0 +1,107 @@ + + + diff --git a/src/stores/cors.ts b/src/stores/cors.ts new file mode 100644 index 00000000..3a13623c --- /dev/null +++ b/src/stores/cors.ts @@ -0,0 +1,67 @@ +import { defineStore } from 'pinia'; +import { getClient } from '~/util/awclient'; + +export interface CorsConfig { + cors: string[]; + cors_regex: string[]; + cors_allow_aw_chrome_extension: boolean; + cors_allow_all_mozilla_extension: boolean; + in_file: string[]; + needs_restart: boolean; +} + +export type MutableCorsConfig = Pick; + +interface State { + config: CorsConfig | null; + loading: boolean; + error: string | null; +} + +export const useCorsStore = defineStore('cors', { + state: (): State => ({ + config: null, + loading: false, + error: null, + }), + actions: { + async load() { + this.loading = true; + this.error = null; + try { + const client = getClient(); + const response = await client.req.get('/0/cors-config'); + this.config = response.data; + } catch (e: any) { + this.error = e.response?.data?.message || e.message || 'Failed to load CORS config'; + } finally { + this.loading = false; + } + }, + async save(newConfig: MutableCorsConfig) { + this.loading = true; + this.error = null; + try { + const client = getClient(); + // Only send the mutable subset to the server + const payload: MutableCorsConfig = { + cors: newConfig.cors, + cors_regex: newConfig.cors_regex, + cors_allow_aw_chrome_extension: newConfig.cors_allow_aw_chrome_extension, + cors_allow_all_mozilla_extension: newConfig.cors_allow_all_mozilla_extension, + }; + await client.req.post('/0/cors-config', payload); + + // Update local state if successful + if (this.config) { + this.config = { ...this.config, ...payload }; + } + } catch (e: any) { + this.error = e.response?.data?.message || e.message || 'Failed to save CORS config'; + throw e; + } finally { + this.loading = false; + } + } + } +}); diff --git a/src/views/settings/Settings.vue b/src/views/settings/Settings.vue index 0328ad18..2a55989d 100644 --- a/src/views/settings/Settings.vue +++ b/src/views/settings/Settings.vue @@ -37,6 +37,11 @@ div hr DeveloperSettings + div.mt-2 + b-btn(v-b-modal.cors-config-modal, variant="outline-primary", size="sm") + | Configure CORS + + CorsConfigModal