Files
figcraft/crates/engine/src/doc_urls_tests.rs
T
Dylan M. Taylor d744379e60 fix(engine): refuse document URLs with shell syntax or a malformed authority
webbrowser on WSL hands an address to cmd.exe /c start, escaping only &
and ^, so a document link holding | (or < > " `) ran what followed it
with the viewer's authority; and an address its parser can't read is
reopened as a file path. Characters never valid in a URL unencoded are now
refused, and an http(s) authority must be made of host characters.
2026-11-03 15:46:13 +00:00

110 lines
3.6 KiB
Rust

//! A URL stored in a document reaches the browser or the system opener, so only the schemes a
//! browser follows on a plain link are allowed ([`crate::doc_urls`]).
use crate::doc_urls::openable;
#[test]
fn web_links_are_opened() {
for url in [
"http://example.com",
"https://example.com",
"https://example.com/a/b?c=d#e",
"HTTPS://EXAMPLE.COM",
"HtTp://example.com",
"mailto:a@b.c",
"mailto:a@b.c?subject=hi",
" https://example.com ",
"https://user:pw@example.com:8443/p",
] {
assert!(openable(url), "{url:?} should open");
}
}
#[test]
fn script_and_handler_schemes_are_refused() {
for url in [
// Runs in the app's own origin on the web build: a saved document would be stored XSS.
"javascript:alert(1)",
"JavaScript:alert(1)",
"jAvAsCrIpT:alert(document.cookie)",
"vbscript:msgbox(1)",
"data:text/html,<script>alert(1)</script>",
"blob:https://example.com/x",
// Reach the file system; a UNC host also sends Windows credentials.
"file:///etc/passwd",
"file://attacker.example/share/x",
"smb://attacker.example/share/x",
"webdav://attacker.example/x",
// Windows application handlers.
"ms-msdt:/id PCWDiagnostic",
"ms-officecmd:x",
"search-ms:query=x",
"shell:startup",
// Anything else a platform may have registered.
"tel:+1234",
"sms:+1234",
"ssh://host",
"ftp://host/x",
"figcraft://join?host=127.0.0.1",
"zoommtg://zoom.us/join?x",
"view-source:https://example.com",
"jar:https://example.com/x!/y",
] {
assert!(!openable(url), "{url:?} must be refused");
}
}
#[test]
fn a_scheme_is_required_and_control_characters_are_refused() {
for url in [
// No scheme: the web opener would resolve these against the app's own page.
"",
" ",
"example.com",
"/etc/passwd",
"//example.com",
"\\\\host\\share\\x",
"about",
// Browsers ignore some control characters inside a scheme, so a smuggled one must not
// leave `javascript` looking like an unknown scheme that we then refuse by luck.
"java\tscript:alert(1)",
"java\nscript:alert(1)",
"java\rscript:alert(1)",
"javascript\u{0}:alert(1)",
"https://example.com/\u{0}",
// A colon in ordinary text is not a scheme.
"see page 2: the diagram",
"1https://example.com",
// The allowed schemes still need their own part.
"mailto:",
"http:",
"http:example.com",
"http:///p",
"https://",
] {
assert!(!openable(url), "{url:?} must be refused");
}
}
#[test]
fn shell_syntax_and_malformed_authorities_are_refused() {
for url in [
// Never valid unencoded, and command syntax to the `cmd.exe /c start` an opener may use.
"https://example.com/?a=b|calc",
"https://example.com/<x>",
"https://example.com/\"x\"",
"https://example.com/a b",
"https://example.com/`x`",
"https://example.com/x^y",
"https://example.com\\x",
// An authority an opener can't parse may be reopened as a file path.
"http://exam ple.com/",
"http://a{b}/",
] {
assert!(!openable(url), "{url:?} must be refused");
}
for url in ["https://example.com/?a=b&c=d%20e", "https://[::1]:8080/x", "https://bücher.example/", "https://a.b-c_d.example:443/"] {
assert!(openable(url), "{url:?} should open");
}
}