diff --git a/README.md b/README.md index 9cac0e393..39e5e569d 100644 --- a/README.md +++ b/README.md @@ -1479,6 +1479,39 @@ GNU Affero General Public License v3.0 (AGPL-3.0) — see [LICENSE](LICENSE) for --- +## Contributors + +Thanks to everyone who has contributed to World Monitor: + +[@SebastienMelki](https://github.com/SebastienMelki), +[@Lib-LOCALE](https://github.com/Lib-LOCALE), +[@lawyered0](https://github.com/lawyered0), +[@elzalem](https://github.com/elzalem), +[@Rau1CS](https://github.com/Rau1CS), +[@Sethispr](https://github.com/Sethispr), +[@InlitX](https://github.com/InlitX), +[@Ahmadhamdan47](https://github.com/Ahmadhamdan47), +[@K35P](https://github.com/K35P), +[@Niboshi-Wasabi](https://github.com/Niboshi-Wasabi), +[@pedroddomingues](https://github.com/pedroddomingues), +[@haosenwang1018](https://github.com/haosenwang1018), +[@aa5064](https://github.com/aa5064), +[@cwnicoletti](https://github.com/cwnicoletti), +[@facusturla](https://github.com/facusturla), +[@toasterbook88](https://github.com/toasterbook88) + +--- + +## Security Acknowledgments + +We thank the following researchers for responsibly disclosing security issues: + +- **Cody Richard** — Disclosed three security findings covering IPC command exposure via DevTools in production builds, renderer-to-sidecar trust boundary analysis, and the global fetch patch credential injection architecture (2025) + +If you discover a vulnerability, please see our [Security Policy](./SECURITY.md) for responsible disclosure guidelines. + +--- +
worldmonitor.app ·
tech.worldmonitor.app ·
diff --git a/SECURITY.md b/SECURITY.md
index f28241d48..cff890e27 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -44,7 +44,8 @@ World Monitor is a client-side intelligence dashboard that aggregates publicly a
### API Keys & Secrets
-- All API keys are stored server-side in Vercel Edge Functions
+- **Web deployment**: API keys are stored server-side in Vercel Edge Functions
+- **Desktop runtime**: API keys are stored in the OS keychain (macOS Keychain / Windows Credential Manager) via a consolidated vault entry, never on disk in plaintext
- No API keys should ever be committed to the repository
- Environment variables (`.env.local`) are gitignored
- The RSS proxy uses domain allowlisting to prevent SSRF
@@ -61,6 +62,15 @@ World Monitor is a client-side intelligence dashboard that aggregates publicly a
- No sensitive data is stored in localStorage or sessionStorage
- External content (RSS feeds, news) is sanitized before rendering
- Map data layers use trusted, vetted data sources
+- Content Security Policy restricts script-src to `'self'` (no unsafe-inline/eval)
+
+### Desktop Runtime Security (Tauri)
+
+- **IPC origin validation**: Sensitive Tauri commands (secrets, cache, token) are gated to trusted windows only; external-origin windows (e.g., YouTube login) are blocked
+- **DevTools**: Disabled in production builds; gated behind an opt-in Cargo feature for development
+- **Sidecar authentication**: A per-session CSPRNG token (`LOCAL_API_TOKEN`) authenticates all renderer-to-sidecar requests, preventing other local processes from accessing the API
+- **Capability isolation**: The YouTube login window runs under a restricted capability with no access to secret or cache IPC commands
+- **Fetch patch trust boundary**: The global fetch interceptor injects the sidecar token with a 5-minute TTL; the renderer is the intended client — if renderer integrity is compromised, Tauri IPC provides strictly more access than the fetch patch
### Data Sources
@@ -77,6 +87,8 @@ The following are **in scope** for security reports:
- Edge function security issues (SSRF, injection, auth bypass)
- XSS or content injection through RSS feeds or external data
- API key exposure or secret leakage
+- Tauri IPC command privilege escalation or capability bypass
+- Sidecar authentication bypass or token leakage
- Dependency vulnerabilities with a viable attack vector
The following are **out of scope**:
diff --git a/middleware.ts b/middleware.ts
index 04c8d1efe..36fdf1ea5 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -12,6 +12,9 @@ const SOCIAL_PREVIEW_UA =
const SOCIAL_PREVIEW_PATHS = new Set(['/api/story', '/api/og-story']);
+// Public endpoints that should never be bot-blocked (version check, etc.)
+const PUBLIC_API_PATHS = new Set(['/api/version']);
+
// Slack uses Slack-ImgProxy to fetch OG images — distinct from Slackbot
const SOCIAL_IMAGE_UA =
/Slack-ImgProxy|Slackbot|twitterbot|facebookexternalhit|linkedinbot|telegrambot|whatsapp|discordbot|redditbot/i;
@@ -33,6 +36,11 @@ export default function middleware(request: Request) {
return;
}
+ // Public endpoints bypass all bot filtering
+ if (PUBLIC_API_PATHS.has(path)) {
+ return;
+ }
+
// Block bots from all API routes
if (BOT_UA.test(ua)) {
return new Response('{"error":"Forbidden"}', {
diff --git a/package.json b/package.json
index 217dc16b5..b9208879a 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
"test:e2e:visual:update:full": "VITE_VARIANT=full playwright test -g \"matches golden screenshots per layer and zoom\" --update-snapshots",
"test:e2e:visual:update:tech": "VITE_VARIANT=tech playwright test -g \"matches golden screenshots per layer and zoom\" --update-snapshots",
"test:e2e:visual:update": "npm run test:e2e:visual:update:full && npm run test:e2e:visual:update:tech",
- "desktop:dev": "npm run version:sync && VITE_DESKTOP_RUNTIME=1 tauri dev",
+ "desktop:dev": "npm run version:sync && VITE_DESKTOP_RUNTIME=1 tauri dev -f devtools",
"desktop:build:full": "npm run version:sync && VITE_VARIANT=full VITE_DESKTOP_RUNTIME=1 tauri build",
"desktop:build:tech": "npm run version:sync && VITE_VARIANT=tech VITE_DESKTOP_RUNTIME=1 tauri build --config src-tauri/tauri.tech.conf.json",
"desktop:build:finance": "npm run version:sync && VITE_VARIANT=finance VITE_DESKTOP_RUNTIME=1 tauri build --config src-tauri/tauri.finance.conf.json",
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 8919cff1c..b63102756 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -4890,7 +4890,7 @@ dependencies = [
[[package]]
name = "world-monitor"
-version = "2.5.6"
+version = "2.5.7"
dependencies = [
"getrandom 0.2.17",
"keyring",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 96e63e35e..5411be19d 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -9,7 +9,7 @@ edition = "2021"
tauri-build = { version = "2", features = [] }
[dependencies]
-tauri = { version = "2", features = ["devtools"] }
+tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
keyring = { version = "3", features = ["apple-native", "windows-native"] }
@@ -19,3 +19,4 @@ getrandom = "0.2"
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
+devtools = ["tauri/devtools"]
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index 10dfb0aea..608b89cfa 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -1,7 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
- "description": "Capabilities for World Monitor main and settings windows",
- "windows": ["main", "settings", "live-channels", "youtube-login"],
+ "description": "Capabilities for World Monitor trusted app windows",
+ "windows": ["main", "settings", "live-channels"],
"permissions": ["core:default"]
}
diff --git a/src-tauri/capabilities/youtube-login.json b/src-tauri/capabilities/youtube-login.json
new file mode 100644
index 000000000..0a25e8b68
--- /dev/null
+++ b/src-tauri/capabilities/youtube-login.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../gen/schemas/desktop-schema.json",
+ "identifier": "youtube-login",
+ "description": "Restricted capabilities for the external-origin YouTube login window",
+ "windows": ["youtube-login"],
+ "permissions": ["core:window:default"]
+}
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 501ac73f2..2552fbf0a 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -16,7 +16,7 @@ use reqwest::Url;
use serde::Serialize;
use serde_json::{Map, Value};
use tauri::menu::{AboutMetadata, Menu, MenuItem, PredefinedMenuItem, Submenu};
-use tauri::{AppHandle, Manager, RunEvent, WebviewUrl, WebviewWindowBuilder, WindowEvent};
+use tauri::{AppHandle, Manager, RunEvent, Webview, WebviewUrl, WebviewWindowBuilder, WindowEvent};
const LOCAL_API_PORT: &str = "46123";
const KEYRING_SERVICE: &str = "world-monitor";
@@ -24,7 +24,9 @@ const LOCAL_API_LOG_FILE: &str = "local-api.log";
const DESKTOP_LOG_FILE: &str = "desktop.log";
const MENU_FILE_SETTINGS_ID: &str = "file.settings";
const MENU_HELP_GITHUB_ID: &str = "help.github";
+#[cfg(feature = "devtools")]
const MENU_HELP_DEVTOOLS_ID: &str = "help.devtools";
+const TRUSTED_WINDOWS: [&str; 3] = ["main", "settings", "live-channels"];
const SUPPORTED_SECRET_KEYS: [&str; 21] = [
"GROQ_API_KEY",
"OPENROUTER_API_KEY",
@@ -195,8 +197,17 @@ fn generate_local_token() -> String {
buf.iter().map(|b| format!("{b:02x}")).collect()
}
+fn require_trusted_window(label: &str) -> Result<(), String> {
+ if TRUSTED_WINDOWS.contains(&label) {
+ Ok(())
+ } else {
+ Err(format!("Command not allowed from window '{label}'"))
+ }
+}
+
#[tauri::command]
-fn get_local_api_token(state: tauri::State<'_, LocalApiState>) -> Result