From aa245efa5d087b71e31807cdbf363055f0af240d Mon Sep 17 00:00:00 2001 From: Anshul Garg Date: Wed, 11 Mar 2026 21:42:19 +0530 Subject: [PATCH 1/2] fix(security): validate resource names in watch and subscribe helpers Add `validate_resource_name()` checks to the `--subscription` flag in `gmail +watch` and to auto-generated slugs in `events +subscribe`. This prevents path traversal and query injection via Pub/Sub resource names, consistent with the existing validation on `--project`. Closes #408 --- ...validate-resource-names-watch-subscribe.md | 5 +++ src/helpers/events/subscribe.rs | 31 ++++++++++++++++ src/helpers/gmail/watch.rs | 36 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 .changeset/validate-resource-names-watch-subscribe.md diff --git a/.changeset/validate-resource-names-watch-subscribe.md b/.changeset/validate-resource-names-watch-subscribe.md new file mode 100644 index 00000000..027f4536 --- /dev/null +++ b/.changeset/validate-resource-names-watch-subscribe.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Validate `--subscription` and auto-generated slug names with `validate_resource_name()` in `gmail +watch` and `events +subscribe` to prevent path traversal and query injection via Pub/Sub resource names diff --git a/src/helpers/events/subscribe.rs b/src/helpers/events/subscribe.rs index dbe6047c..9e4e1ec3 100644 --- a/src/helpers/events/subscribe.rs +++ b/src/helpers/events/subscribe.rs @@ -132,6 +132,7 @@ pub(super) async fn handle_subscribe( // Generate descriptive names from event types // e.g. "google.workspace.drive.file.v1.updated" -> "drive-file-updated" let slug = derive_slug_from_event_types(&event_types_str); + crate::validate::validate_resource_name(&slug)?; let suffix = format!("{:08x}", rand::random::()); let topic = format!("projects/{project}/topics/gws-{slug}-{suffix}"); let sub = format!("projects/{project}/subscriptions/gws-{slug}-{suffix}"); @@ -715,6 +716,36 @@ mod tests { assert!(events.is_empty()); } + #[test] + fn test_parse_subscribe_args_subscription_traversal_rejected() { + let matches = + make_matches_subscribe(&["test", "--subscription", "projects/../admin/subs/x"]); + let result = parse_subscribe_args(&matches); + assert!(result.is_err()); + let msg = result.unwrap_err().to_string(); + assert!(msg.contains("path traversal")); + } + + #[test] + fn test_parse_subscribe_args_subscription_control_chars_rejected() { + let matches = make_matches_subscribe(&["test", "--subscription", "subs/bad\0name"]); + let result = parse_subscribe_args(&matches); + assert!(result.is_err()); + let msg = result.unwrap_err().to_string(); + assert!(msg.contains("invalid characters")); + } + + #[test] + fn test_derive_slug_no_control_chars() { + // Normal event types produce clean slugs + let types = vec!["google.workspace.drive.file.v1.updated"]; + let slug = derive_slug_from_event_types(&types); + assert!( + crate::validate::validate_resource_name(&slug).is_ok(), + "Generated slug should pass resource name validation" + ); + } + #[test] fn test_handle_subscribe_validation_missing_target() { let config = SubscribeConfigBuilder::default() diff --git a/src/helpers/gmail/watch.rs b/src/helpers/gmail/watch.rs index 86401799..d1abf8e0 100644 --- a/src/helpers/gmail/watch.rs +++ b/src/helpers/gmail/watch.rs @@ -539,6 +539,10 @@ fn parse_watch_args(matches: &ArgMatches) -> Result { .map(|dir| crate::validate::validate_safe_output_dir(dir)) .transpose()?; + if let Some(subscription) = matches.get_one::("subscription") { + crate::validate::validate_resource_name(subscription)?; + } + Ok(WatchConfig { project: matches.get_one::("project").cloned(), subscription: matches.get_one::("subscription").cloned(), @@ -709,6 +713,38 @@ mod tests { assert!(!config.cleanup); } + #[test] + fn test_parse_watch_args_subscription_traversal_rejected() { + let matches = make_matches_watch(&["test", "--subscription", "projects/../admin/subs/x"]); + let result = parse_watch_args(&matches); + assert!(result.is_err()); + let msg = result.unwrap_err().to_string(); + assert!(msg.contains("path traversal")); + } + + #[test] + fn test_parse_watch_args_subscription_control_chars_rejected() { + let matches = make_matches_watch(&["test", "--subscription", "subs/bad\0name"]); + let result = parse_watch_args(&matches); + assert!(result.is_err()); + let msg = result.unwrap_err().to_string(); + assert!(msg.contains("invalid characters")); + } + + #[test] + fn test_parse_watch_args_valid_subscription() { + let matches = make_matches_watch(&[ + "test", + "--subscription", + "projects/my-proj/subscriptions/my-sub", + ]); + let config = parse_watch_args(&matches).unwrap(); + assert_eq!( + config.subscription.unwrap(), + "projects/my-proj/subscriptions/my-sub" + ); + } + #[test] fn test_parse_watch_args_invalid_numbers() { let matches = make_matches_watch(&[ From 6477b53013c7e48c722bc4211c252fe8a57edfd8 Mon Sep 17 00:00:00 2001 From: Anshul Garg Date: Wed, 11 Mar 2026 22:22:00 +0530 Subject: [PATCH 2/2] chore: trigger CLA re-check