diff --git a/.env.example b/.env.example index e802d84..cbc1afd 100644 --- a/.env.example +++ b/.env.example @@ -24,7 +24,8 @@ FIREBASE_PROJECT_ID=mostro-test FIREBASE_SERVICE_ACCOUNT_PATH=/path/to/service-account.json # UnifiedPush Configuration -UNIFIEDPUSH_ENABLED=true +# Opt-in: the dispatch path POSTs to the device token treated as a URL. +UNIFIEDPUSH_ENABLED=false # FCM Configuration FCM_ENABLED=true diff --git a/docs/configuration.md b/docs/configuration.md index c1ed28b..23e5ae8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -72,7 +72,7 @@ To turn the filter on/off without rebuilding, flip | Variable | Default | Description | |---------------------------------|---------|--------------------------------------------------------------------------------------------| | `FCM_ENABLED` | `true` | Enable Firebase Cloud Messaging backend | -| `UNIFIEDPUSH_ENABLED` | `true` | Enable UnifiedPush backend | +| `UNIFIEDPUSH_ENABLED` | `false` | Enable UnifiedPush backend. Opt-in on purpose: the dispatch path POSTs to the client-supplied device token treated as a URL, so the backend stays off unless set explicitly. | | `FIREBASE_PROJECT_ID` | - | Firebase project ID, required when `FCM_ENABLED=true` | | `FIREBASE_SERVICE_ACCOUNT_PATH` | - | Absolute path to the Firebase service-account JSON. If missing or unreadable, FCM is disabled at startup with a warning; the server keeps running. | | `BATCH_DELAY_MS` | `5000` | Reserved (declared on `PushConfig`; not currently consumed) | diff --git a/src/config.rs b/src/config.rs index f2884af..bba8af2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -102,8 +102,12 @@ impl Config { fcm_enabled: env::var("FCM_ENABLED") .unwrap_or_else(|_| "true".to_string()) .parse()?, + // Default false: the UnifiedPush dispatch path POSTs to the + // client-supplied device token treated as a URL, so enabling + // the backend by omission opens an SSRF surface. Operators + // opt in explicitly. unifiedpush_enabled: env::var("UNIFIEDPUSH_ENABLED") - .unwrap_or_else(|_| "true".to_string()) + .unwrap_or_else(|_| "false".to_string()) .parse()?, batch_delay_ms: env::var("BATCH_DELAY_MS") .unwrap_or_else(|_| "5000".to_string()) @@ -246,4 +250,63 @@ mod tests { msg ); } + + /// UnifiedPush must be opt-in. Its dispatch path POSTs to the + /// client-supplied device token treated as a URL, so a deployment that + /// simply forgets the variable must not end up with the backend live. + #[test] + fn unifiedpush_defaults_to_disabled() { + let _guard = ENV_MUTEX.lock().unwrap(); + std::env::remove_var("UNIFIEDPUSH_ENABLED"); + std::env::set_var("NOSTR_RELAYS", "wss://relay.example.com"); + + let result = Config::from_env(); + + std::env::remove_var("NOSTR_RELAYS"); + + let config = result.expect("Config::from_env MUST succeed on defaults"); + assert!( + !config.push.unifiedpush_enabled, + "UNIFIEDPUSH_ENABLED MUST default to false" + ); + } + + /// The opt-in still works: setting the variable explicitly enables it. + #[test] + fn unifiedpush_honours_explicit_opt_in() { + let _guard = ENV_MUTEX.lock().unwrap(); + std::env::set_var("UNIFIEDPUSH_ENABLED", "true"); + std::env::set_var("NOSTR_RELAYS", "wss://relay.example.com"); + + let result = Config::from_env(); + + std::env::remove_var("UNIFIEDPUSH_ENABLED"); + std::env::remove_var("NOSTR_RELAYS"); + + let config = result.expect("Config::from_env MUST succeed on explicit opt-in"); + assert!( + config.push.unifiedpush_enabled, + "UNIFIEDPUSH_ENABLED=true MUST enable the backend" + ); + } + + /// FCM keeps its permissive default: it does not take a client-supplied + /// URL, so the fail-open concern that motivates the UnifiedPush default + /// does not apply, and flipping it would change existing deployments. + #[test] + fn fcm_default_is_unchanged() { + let _guard = ENV_MUTEX.lock().unwrap(); + std::env::remove_var("FCM_ENABLED"); + std::env::set_var("NOSTR_RELAYS", "wss://relay.example.com"); + + let result = Config::from_env(); + + std::env::remove_var("NOSTR_RELAYS"); + + let config = result.expect("Config::from_env MUST succeed on defaults"); + assert!( + config.push.fcm_enabled, + "FCM_ENABLED MUST keep defaulting to true" + ); + } }