diff --git a/src-tauri/src/commands/secure_paste/reveal.rs b/src-tauri/src/commands/secure_paste/reveal.rs index 9f33098..48d1eee 100644 --- a/src-tauri/src/commands/secure_paste/reveal.rs +++ b/src-tauri/src/commands/secure_paste/reveal.rs @@ -13,6 +13,18 @@ pub struct PasteRevealPayload { pub error: Option, } +/// Paste ids are 22-char base64url ShortGuids issued by the API. The id is interpolated into +/// the DELETE path, so anything else (e.g. `../`, `?`) must be rejected before it gets there. +fn is_paste_id(s: &str) -> bool { + s.len() == 22 && is_base64url(s) +} + +fn is_base64url(s: &str) -> bool { + !s.is_empty() + && s.bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') +} + /// Parse `cmdv://paste?id={pasteId}&key={encryptionKey}`. pub fn parse_paste_deep_link(url: &str) -> Option<(String, String)> { let url = url.trim(); @@ -29,7 +41,8 @@ pub fn parse_paste_deep_link(url: &str) -> Option<(String, String)> { _ => {} } } - Some((paste_id?, key?)) + let (paste_id, key) = (paste_id?, key?); + (is_paste_id(&paste_id) && is_base64url(&key)).then_some((paste_id, key)) } fn percent_decode(input: &str) -> String { @@ -104,10 +117,29 @@ mod tests { #[test] fn parse_valid_paste_deep_link() { - let url = "cmdv://paste?id=abc123&key=YWJj"; + let url = "cmdv://paste?id=AbCdEfGhIjKlMnOpQrSt_-&key=S2V5S2V5S2V5S2V5S2V5S2"; let (id, key) = parse_paste_deep_link(url).unwrap(); - assert_eq!(id, "abc123"); - assert_eq!(key, "YWJj"); + assert_eq!(id, "AbCdEfGhIjKlMnOpQrSt_-"); + assert_eq!(key, "S2V5S2V5S2V5S2V5S2V5S2"); + } + + #[test] + fn parse_rejects_id_that_is_not_a_short_guid() { + for id in [ + "..%2Fauth%2Fsessions%2F0f8fad5b-d9cb-469f-a165-70867728950e", + "abc%3Fx%3D1", + "abc123", + "AbCdEfGhIjKlMnOpQrSt_-x", + ] { + let url = format!("cmdv://paste?id={id}&key=S2V5S2V5S2V5S2V5S2V5S2"); + assert!(parse_paste_deep_link(&url).is_none(), "accepted id {id}"); + } + } + + #[test] + fn parse_rejects_key_outside_base64url() { + let url = "cmdv://paste?id=AbCdEfGhIjKlMnOpQrSt_-&key=a%2Fb%2Bc"; + assert!(parse_paste_deep_link(url).is_none()); } #[test]