mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-27 20:07:19 -06:00
- `ui.render` and `ui.screenshot` without a `path` return the PNG as base64 `png` in the reply instead of writing a fixed name into the shared temporary folder, where another user could have planted a link to one of this user's files; the MCP bridge's `render_page` and `screenshot` use that and no longer round-trip through `temp_dir()/wordcraft-mcp-*.png`. A given `path` still works as before (#678). - One allow-list, `wordcraft_doc::link`: `is_web_or_mail_link` for Ctrl/⌘-click and `is_safe_href` (web, mail, `#bookmark`, relative path; schemes read the way browsers do, so `java\nscript:` is a script) for the HTML and Markdown exporters and the HTML importer. Scripts and other schemes export as plain text (#679). - Tests: the allow-list, exporters with hostile links, the MCP bridge against a fake app (no `path` ever asked for), the control channel's in-reply PNGs. Co-authored-by: petycomppety <30414255+petycomppety@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
82 lines
3.3 KiB
Rust
82 lines
3.3 KiB
Rust
//! Which hyperlink targets may leave the document: followed by the OS, or written into an
|
|
//! exported HTML/Markdown file where a browser would run them.
|
|
|
|
/// Whether Ctrl/⌘+click may hand a document's hyperlink to the system: web and email links only,
|
|
/// so a document can't launch `file:` paths, programs or custom-scheme handlers. Exact: a leading
|
|
/// space or a control character makes it not a link.
|
|
pub fn is_web_or_mail_link(url: &str) -> bool {
|
|
let Some((scheme, _)) = url.split_once(':') else { return false };
|
|
["http", "https", "mailto"].iter().any(|s| scheme.eq_ignore_ascii_case(s))
|
|
}
|
|
|
|
/// Whether a hyperlink may be written as an `href` (or Markdown destination) in an exported file:
|
|
/// a web or email link, a bookmark (`#name`) or a relative path. Scripts (`javascript:`,
|
|
/// `vbscript:`), `data:`, `file:` and every other scheme are refused, read the way a browser reads
|
|
/// a URL: ASCII control characters and spaces are dropped first, so `java\nscript:` is a script.
|
|
pub fn is_safe_href(url: &str) -> bool {
|
|
match scheme(url) {
|
|
Some(s) => matches!(s.as_str(), "http" | "https" | "mailto"),
|
|
None => !url.trim().is_empty(),
|
|
}
|
|
}
|
|
|
|
/// The scheme a browser would see in `url` (RFC 3986 §3.1: a letter, then letters, digits, `+`,
|
|
/// `-` or `.`, up to the first `:`), lower-cased; `None` for a relative reference.
|
|
fn scheme(url: &str) -> Option<String> {
|
|
let cleaned: String = url.chars().filter(|c| !c.is_ascii_control() && *c != ' ').collect();
|
|
let (s, _) = cleaned.split_once(':')?;
|
|
let mut chars = s.chars();
|
|
let first = chars.next()?;
|
|
(first.is_ascii_alphabetic() && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))).then(|| s.to_ascii_lowercase())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn only_web_and_mail_links_are_followable() {
|
|
for ok in ["https://example.com/a?b#c", "http://example.com", "HTTPS://EXAMPLE.COM", "mailto:someone@example.com"] {
|
|
assert!(is_web_or_mail_link(ok), "{ok}");
|
|
}
|
|
for bad in [
|
|
"file:///etc/passwd",
|
|
"C:\\Windows\\System32\\calc.exe",
|
|
"/usr/bin/xterm",
|
|
"javascript:alert(1)",
|
|
"ms-msdt:/id",
|
|
" https://example.com",
|
|
"https\u{0}://example.com",
|
|
"",
|
|
"example.com",
|
|
"#top",
|
|
] {
|
|
assert!(!is_web_or_mail_link(bad), "{bad:?}");
|
|
}
|
|
}
|
|
|
|
/// An exported `href` may be a web/mail link, a bookmark or a relative path: never a script
|
|
/// or another scheme, however a browser would normalise it.
|
|
#[test]
|
|
fn exported_hrefs_allow_bookmarks_and_relative_paths_but_no_scripts() {
|
|
for ok in ["https://example.com/x", "mailto:a@b.c", "#top", "other.html", "docs/guide.html#s2", "../up.html", "?q=1"] {
|
|
assert!(is_safe_href(ok), "{ok}");
|
|
}
|
|
for bad in [
|
|
"javascript:alert(1)",
|
|
"JavaScript:alert(1)",
|
|
"java\nscript:alert(1)",
|
|
"\tjavascript:alert(1)",
|
|
" javascript:alert(1)",
|
|
"java\u{0}script:alert(1)",
|
|
"vbscript:MsgBox(1)",
|
|
"data:text/html,<script>alert(1)</script>",
|
|
"file:///etc/passwd",
|
|
"ms-msdt:/id",
|
|
"",
|
|
] {
|
|
assert!(!is_safe_href(bad), "{bad:?}");
|
|
}
|
|
}
|
|
}
|