From a0e01f04d3d65d15b2d0a759b8357949c10bea12 Mon Sep 17 00:00:00 2001 From: Illia Polosukhin Date: Sat, 14 Feb 2026 12:54:20 -0800 Subject: [PATCH] fix: address critical/high audit findings across WASM sub-crates - Telegram: remove .unwrap() panic on workspace_read (owner_id check) - WhatsApp: use configured api_version instead of hardcoded v18.0 - WhatsApp: log config parse errors before falling back to defaults - Slack: log serialization errors in emit_message and json_response - Google Docs: safe array access for batch update replies - Google Sheets: safe array access for add_sheet replies - Google Calendar: fix doc comment secret name mismatch - Gmail: avoid unnecessary String allocation in UNREAD check Co-Authored-By: Claude Opus 4.6 --- channels-src/slack/src/lib.rs | 16 +++++++++++++-- channels-src/telegram/src/lib.rs | 28 +++++++++------------------ channels-src/whatsapp/src/lib.rs | 29 ++++++++++++++++++++++------ tools-src/gmail/src/api.rs | 4 ++-- tools-src/google-calendar/src/lib.rs | 2 +- tools-src/google-docs/src/api.rs | 9 +++++++-- tools-src/google-sheets/src/api.rs | 8 +++++++- 7 files changed, 63 insertions(+), 33 deletions(-) diff --git a/channels-src/slack/src/lib.rs b/channels-src/slack/src/lib.rs index 5cf7b10d..e4f47692 100644 --- a/channels-src/slack/src/lib.rs +++ b/channels-src/slack/src/lib.rs @@ -338,7 +338,13 @@ fn emit_message( team_id, }; - let metadata_json = serde_json::to_string(&metadata).unwrap_or_else(|_| "{}".to_string()); + let metadata_json = serde_json::to_string(&metadata).unwrap_or_else(|e| { + channel_host::log( + channel_host::LogLevel::Error, + &format!("Failed to serialize Slack metadata: {}", e), + ); + "{}".to_string() + }); // Strip @ mentions of the bot from the text for cleaner messages let cleaned_text = strip_bot_mention(&text); @@ -366,7 +372,13 @@ fn strip_bot_mention(text: &str) -> String { /// Create a JSON HTTP response. fn json_response(status: u16, value: serde_json::Value) -> OutgoingHttpResponse { - let body = serde_json::to_vec(&value).unwrap_or_default(); + let body = serde_json::to_vec(&value).unwrap_or_else(|e| { + channel_host::log( + channel_host::LogLevel::Error, + &format!("Failed to serialize JSON response: {}", e), + ); + Vec::new() + }); let headers = serde_json::json!({"Content-Type": "application/json"}); OutgoingHttpResponse { diff --git a/channels-src/telegram/src/lib.rs b/channels-src/telegram/src/lib.rs index 08e82804..a7f7f5cb 100644 --- a/channels-src/telegram/src/lib.rs +++ b/channels-src/telegram/src/lib.rs @@ -285,11 +285,7 @@ impl Guest for TelegramChannel { } // Persist dm_policy and allow_from for DM pairing in handle_message - let dm_policy = config - .dm_policy - .as_deref() - .unwrap_or("pairing") - .to_string(); + let dm_policy = config.dm_policy.as_deref().unwrap_or("pairing").to_string(); let _ = channel_host::workspace_write(DM_POLICY_PATH, &dm_policy); let allow_from_json = serde_json::to_string(&config.allow_from.unwrap_or_default()) @@ -844,8 +840,8 @@ fn send_pairing_reply(chat_id: i64, code: &str) -> Result<(), String> { "parse_mode": "Markdown", }); - let payload_bytes = serde_json::to_vec(&payload) - .map_err(|e| format!("Failed to serialize payload: {}", e))?; + let payload_bytes = + serde_json::to_vec(&payload).map_err(|e| format!("Failed to serialize payload: {}", e))?; let headers = serde_json::json!({ "Content-Type": "application/json" @@ -915,15 +911,10 @@ fn handle_message(message: TelegramMessage) { let is_private = message.chat.chat_type == "private"; // Owner validation: when owner_id is set, only that user can message - let owner_configured = channel_host::workspace_read(OWNER_ID_PATH) - .map(|s| !s.is_empty()) - .unwrap_or(false); + let owner_id_str = channel_host::workspace_read(OWNER_ID_PATH).filter(|s| !s.is_empty()); - if owner_configured { - if let Ok(owner_id) = channel_host::workspace_read(OWNER_ID_PATH) - .unwrap() - .parse::() - { + if let Some(ref id_str) = owner_id_str { + if let Ok(owner_id) = id_str.parse::() { if from.id != owner_id { channel_host::log( channel_host::LogLevel::Debug, @@ -937,8 +928,8 @@ fn handle_message(message: TelegramMessage) { } } else if is_private { // No owner_id: apply dm_policy for private chats - let dm_policy = channel_host::workspace_read(DM_POLICY_PATH) - .unwrap_or_else(|| "pairing".to_string()); + let dm_policy = + channel_host::workspace_read(DM_POLICY_PATH).unwrap_or_else(|| "pairing".to_string()); if dm_policy != "open" { // Build effective allow list: config allow_from + pairing store @@ -1001,8 +992,7 @@ fn handle_message(message: TelegramMessage) { if !respond_to_all { let has_command = content.starts_with('/'); - let bot_username = channel_host::workspace_read(BOT_USERNAME_PATH) - .unwrap_or_default(); + let bot_username = channel_host::workspace_read(BOT_USERNAME_PATH).unwrap_or_default(); let has_bot_mention = if bot_username.is_empty() { content.contains('@') } else { diff --git a/channels-src/whatsapp/src/lib.rs b/channels-src/whatsapp/src/lib.rs index 27d79e2c..7913fcd4 100644 --- a/channels-src/whatsapp/src/lib.rs +++ b/channels-src/whatsapp/src/lib.rs @@ -254,10 +254,19 @@ struct WhatsAppChannel; impl Guest for WhatsAppChannel { fn on_start(config_json: String) -> Result { - let config: WhatsAppConfig = serde_json::from_str(&config_json).unwrap_or(WhatsAppConfig { - api_version: default_api_version(), - reply_to_message: default_reply_to_message(), - }); + let config: WhatsAppConfig = match serde_json::from_str(&config_json) { + Ok(c) => c, + Err(e) => { + channel_host::log( + channel_host::LogLevel::Warn, + &format!("Failed to parse WhatsApp config, using defaults: {}", e), + ); + WhatsAppConfig { + api_version: default_api_version(), + reply_to_message: default_reply_to_message(), + } + } + }; channel_host::log( channel_host::LogLevel::Info, @@ -267,6 +276,9 @@ impl Guest for WhatsAppChannel { ), ); + // Persist api_version in workspace so on_respond() can read it + let _ = channel_host::workspace_write("channels/whatsapp/api_version", &config.api_version); + // WhatsApp Cloud API is webhook-only, no polling available Ok(ChannelConfig { display_name: "WhatsApp".to_string(), @@ -327,11 +339,16 @@ impl Guest for WhatsAppChannel { let metadata: WhatsAppMessageMetadata = serde_json::from_str(&response.metadata_json) .map_err(|e| format!("Failed to parse metadata: {}", e))?; + // Read api_version from workspace (set during on_start), fallback to default + let api_version = channel_host::workspace_read("channels/whatsapp/api_version") + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| "v18.0".to_string()); + // Build WhatsApp API URL with token placeholder // Host will replace {WHATSAPP_ACCESS_TOKEN} with actual token in Authorization header let api_url = format!( - "https://graph.facebook.com/v18.0/{}/messages", - metadata.phone_number_id + "https://graph.facebook.com/{}/{}/messages", + api_version, metadata.phone_number_id ); // Build sendMessage payload diff --git a/tools-src/gmail/src/api.rs b/tools-src/gmail/src/api.rs index f1bc9764..7058e10f 100644 --- a/tools-src/gmail/src/api.rs +++ b/tools-src/gmail/src/api.rs @@ -136,7 +136,7 @@ fn parse_message(v: &serde_json::Value) -> Message { date: get_header(payload, "Date"), body: extract_body(payload), snippet: v["snippet"].as_str().unwrap_or("").to_string(), - is_unread: label_ids.contains(&"UNREAD".to_string()), + is_unread: label_ids.iter().any(|l| l == "UNREAD"), label_ids, } } @@ -198,7 +198,7 @@ pub fn list_messages( to: get_header(payload, "To"), date: get_header(payload, "Date"), snippet: msg["snippet"].as_str().unwrap_or("").to_string(), - is_unread: label_ids.contains(&"UNREAD".to_string()), + is_unread: label_ids.iter().any(|l| l == "UNREAD"), label_ids, }); } diff --git a/tools-src/google-calendar/src/lib.rs b/tools-src/google-calendar/src/lib.rs index 70f62d63..ed73ec8f 100644 --- a/tools-src/google-calendar/src/lib.rs +++ b/tools-src/google-calendar/src/lib.rs @@ -6,7 +6,7 @@ //! # Capabilities Required //! //! - HTTP: `www.googleapis.com/calendar/v3/*` (GET, POST, PUT, PATCH, DELETE) -//! - Secrets: `google_calendar_token` (OAuth 2.0 token, injected automatically) +//! - Secrets: `google_oauth_token` (OAuth 2.0 token, injected automatically) //! //! # Supported Actions //! diff --git a/tools-src/google-docs/src/api.rs b/tools-src/google-docs/src/api.rs index 185ae979..f31ccf69 100644 --- a/tools-src/google-docs/src/api.rs +++ b/tools-src/google-docs/src/api.rs @@ -269,8 +269,13 @@ pub fn replace_text( let parsed = batch_update_raw(document_id, vec![request])?; - let occurrences = parsed["replies"][0]["replaceAllText"]["occurrencesChanged"] - .as_i64() + let first_reply = parsed["replies"].as_array().and_then(|arr| arr.first()); + let occurrences = first_reply + .map(|r| { + r["replaceAllText"]["occurrencesChanged"] + .as_i64() + .unwrap_or(0) + }) .unwrap_or(0); Ok(ReplaceResult { diff --git a/tools-src/google-sheets/src/api.rs b/tools-src/google-sheets/src/api.rs index 4d7af5ca..6ece13b5 100644 --- a/tools-src/google-sheets/src/api.rs +++ b/tools-src/google-sheets/src/api.rs @@ -330,7 +330,13 @@ pub fn add_sheet(spreadsheet_id: &str, title: &str) -> Result