Skip to content

Commit b03428d

Browse files
fix(security): validate resource IDs in docs, sheets, calendar, and drive helpers
Adds validate_resource_name() calls before embedding user-supplied IDs in API request params. Rejects path traversal, control characters, and URL injection via ?, #, and % characters — consistent with AGENTS.md requirements for AI-agent-safe input handling. Affected helpers: - docs +write: --document - sheets +append and +read: --spreadsheet - calendar +insert: --calendar - drive +upload: --parent Includes rejection tests for traversal and query injection in each helper.
1 parent a3768d0 commit b03428d

5 files changed

Lines changed: 105 additions & 7 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@googleworkspace/cli": patch
3+
---
4+
5+
Validate resource IDs in docs, sheets, calendar, and drive helpers
6+
7+
`document_id` (docs `+write`), `spreadsheet_id` (sheets `+append` and `+read`),
8+
`calendar_id` (calendar `+insert`), and `parent_id` (drive `+upload`) are now
9+
validated with `validate_resource_name()` before use. This rejects path traversal
10+
segments (`../`), control characters, and URL-special characters (`?`, `#`, `%`)
11+
that could be injected by adversarial AI-agent inputs.

crates/google-workspace-cli/src/helpers/calendar.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,8 @@ fn build_insert_request(
423423
matches: &ArgMatches,
424424
doc: &crate::discovery::RestDescription,
425425
) -> Result<(String, String, Vec<String>), GwsError> {
426-
let calendar_id = matches.get_one::<String>("calendar").unwrap();
426+
let calendar_id =
427+
crate::validate::validate_resource_name(matches.get_one::<String>("calendar").unwrap())?;
427428
let summary = matches.get_one::<String>("summary").unwrap();
428429
let start = matches.get_one::<String>("start").unwrap();
429430
let end = matches.get_one::<String>("end").unwrap();
@@ -576,6 +577,26 @@ mod tests {
576577
assert_eq!(scopes[0], "https://scope");
577578
}
578579

580+
#[test]
581+
fn test_build_insert_request_rejects_traversal_calendar_id() {
582+
let doc = make_mock_doc();
583+
let matches = make_matches_insert(&[
584+
"test",
585+
"--calendar",
586+
"../../.ssh/id_rsa",
587+
"--summary",
588+
"X",
589+
"--start",
590+
"2024-01-01T10:00:00Z",
591+
"--end",
592+
"2024-01-01T11:00:00Z",
593+
]);
594+
assert!(
595+
build_insert_request(&matches, &doc).is_err(),
596+
"path traversal in --calendar must be rejected"
597+
);
598+
}
599+
579600
#[test]
580601
fn test_build_insert_request_with_meet() {
581602
let doc = make_mock_doc();

crates/google-workspace-cli/src/helpers/docs.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ fn build_write_request(
120120
matches: &ArgMatches,
121121
doc: &crate::discovery::RestDescription,
122122
) -> Result<(String, String, Vec<String>), GwsError> {
123-
let document_id = matches.get_one::<String>("document").unwrap();
123+
let document_id =
124+
crate::validate::validate_resource_name(matches.get_one::<String>("document").unwrap())?;
124125
let text = matches.get_one::<String>("text").unwrap();
125126

126127
let documents_res = doc
@@ -203,4 +204,20 @@ mod tests {
203204
assert!(body.contains("endOfSegmentLocation"));
204205
assert_eq!(scopes[0], "https://scope");
205206
}
207+
208+
#[test]
209+
fn test_build_write_request_rejects_traversal_document_id() {
210+
let doc = make_mock_doc();
211+
let matches = make_matches_write(&["test", "--document", "../../.ssh/id_rsa", "--text", "x"]);
212+
let result = build_write_request(&matches, &doc);
213+
assert!(result.is_err(), "path traversal in --document must be rejected");
214+
}
215+
216+
#[test]
217+
fn test_build_write_request_rejects_query_injection_document_id() {
218+
let doc = make_mock_doc();
219+
let matches = make_matches_write(&["test", "--document", "abc?evil=1", "--text", "x"]);
220+
let result = build_write_request(&matches, &doc);
221+
assert!(result.is_err(), "'?' in --document must be rejected");
222+
}
206223
}

crates/google-workspace-cli/src/helpers/drive.rs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ TIPS:
9191
})?;
9292

9393
// Build metadata
94-
let metadata = build_metadata(&filename, parent_id.map(|s| s.as_str()));
94+
let metadata = build_metadata(&filename, parent_id.map(|s| s.as_str()))?;
9595

9696
let body_str = metadata.to_string();
9797

@@ -142,16 +142,17 @@ fn determine_filename(file_path: &str, name_arg: Option<&str>) -> Result<String,
142142
}
143143
}
144144

145-
fn build_metadata(filename: &str, parent_id: Option<&str>) -> Value {
145+
fn build_metadata(filename: &str, parent_id: Option<&str>) -> Result<Value, GwsError> {
146146
let mut metadata = json!({
147147
"name": filename
148148
});
149149

150150
if let Some(parent) = parent_id {
151+
crate::validate::validate_resource_name(parent)?;
151152
metadata["parents"] = json!([parent]);
152153
}
153154

154-
metadata
155+
Ok(metadata)
155156
}
156157

157158
#[cfg(test)]
@@ -182,15 +183,31 @@ mod tests {
182183

183184
#[test]
184185
fn test_build_metadata_no_parent() {
185-
let meta = build_metadata("file.txt", None);
186+
let meta = build_metadata("file.txt", None).unwrap();
186187
assert_eq!(meta["name"], "file.txt");
187188
assert!(meta.get("parents").is_none());
188189
}
189190

190191
#[test]
191192
fn test_build_metadata_with_parent() {
192-
let meta = build_metadata("file.txt", Some("folder123"));
193+
let meta = build_metadata("file.txt", Some("folder123")).unwrap();
193194
assert_eq!(meta["name"], "file.txt");
194195
assert_eq!(meta["parents"][0], "folder123");
195196
}
197+
198+
#[test]
199+
fn test_build_metadata_rejects_traversal_parent_id() {
200+
assert!(
201+
build_metadata("file.txt", Some("../../.ssh/id_rsa")).is_err(),
202+
"path traversal in --parent must be rejected"
203+
);
204+
}
205+
206+
#[test]
207+
fn test_build_metadata_rejects_query_injection_parent_id() {
208+
assert!(
209+
build_metadata("file.txt", Some("folder?evil=1")).is_err(),
210+
"'?' in --parent must be rejected"
211+
);
212+
}
196213
}

crates/google-workspace-cli/src/helpers/sheets.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ fn build_append_request(
209209
config: &AppendConfig,
210210
doc: &crate::discovery::RestDescription,
211211
) -> Result<(String, String, Vec<String>), GwsError> {
212+
crate::validate::validate_resource_name(&config.spreadsheet_id)?;
213+
212214
let spreadsheets_res = doc
213215
.resources
214216
.get("spreadsheets")
@@ -240,6 +242,8 @@ fn build_read_request(
240242
config: &ReadConfig,
241243
doc: &crate::discovery::RestDescription,
242244
) -> Result<(String, Vec<String>), GwsError> {
245+
crate::validate::validate_resource_name(&config.spreadsheet_id)?;
246+
243247
// ... resource lookup omitted for brevity ...
244248
let spreadsheets_res = doc
245249
.resources
@@ -309,6 +313,7 @@ pub fn parse_append_args(matches: &ArgMatches) -> AppendConfig {
309313
}
310314
}
311315

316+
312317
/// Configuration for reading values from a spreadsheet.
313318
pub struct ReadConfig {
314319
pub spreadsheet_id: String,
@@ -522,4 +527,31 @@ mod tests {
522527
assert!(subcommands.contains(&"+append"));
523528
assert!(subcommands.contains(&"+read"));
524529
}
530+
531+
#[test]
532+
fn test_build_append_request_rejects_traversal() {
533+
let doc = make_mock_doc();
534+
let config = AppendConfig {
535+
spreadsheet_id: "../../.ssh/id_rsa".to_string(),
536+
range: "A1".to_string(),
537+
values: vec![vec!["x".to_string()]],
538+
};
539+
assert!(
540+
build_append_request(&config, &doc).is_err(),
541+
"path traversal in spreadsheet ID must be rejected"
542+
);
543+
}
544+
545+
#[test]
546+
fn test_build_read_request_rejects_query_injection() {
547+
let doc = make_mock_doc();
548+
let config = ReadConfig {
549+
spreadsheet_id: "abc?evil=1".to_string(),
550+
range: "A1:B2".to_string(),
551+
};
552+
assert!(
553+
build_read_request(&config, &doc).is_err(),
554+
"'?' in spreadsheet ID must be rejected"
555+
);
556+
}
525557
}

0 commit comments

Comments
 (0)