Reject non-digit object ids in MCP object resource URIs

Found while testing crates/mcp/src/resources.rs:191 (read_object)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-11-08 21:10:17 +01:00
co-authored by Claude Haiku 5.5
parent c0893584d4
commit 5a4772d24a
+15
View File
@@ -188,6 +188,10 @@ fn decode(s: &str) -> String {
/// caller turns it into `-32602`); other failures (no document, an unreachable app) stay
/// backend errors.
fn read_object(b: &mut dyn Backend, id: &str) -> Result<Option<Value>, String> {
// `parse` alone would accept a leading `+`.
if id.is_empty() || !id.bytes().all(|c| c.is_ascii_digit()) {
return Ok(None);
}
let Ok(id) = id.parse::<u64>() else { return Ok(None) };
match b.call("engine.execute", json!({"command": "document.node", "params": {"id": id, "summary": true}})) {
Ok(v) => Ok(Some(v)),
@@ -268,6 +272,17 @@ mod tests {
assert!(matches!(read(empty.as_mut(), "vectorcraft://object/1"), Err(ReadError::Backend(_))));
}
#[test]
fn object_id_must_be_plain_digits() {
let mut b = Box::new(crate::Headless::with_document());
let made = b.call("engine.execute", json!({"command": "shape.rectangle", "params": {"x": 10, "y": 10, "width": 50, "height": 40}})).unwrap();
let id = made["id"].as_i64().expect("shape.rectangle returns an integer id");
for uri in [format!("vectorcraft://object/+{id}"), format!("vectorcraft://object/-{id}")] {
assert!(matches!(read(b.as_mut(), &uri), Err(ReadError::Invalid(_))), "{uri}");
}
assert!(read(b.as_mut(), &format!("vectorcraft://object/{id}")).is_ok());
}
#[test]
fn an_object_is_readable_once_it_exists() {
let mut b = Box::new(crate::Headless::with_document());