Security: ask before opening links from documents (#90, #91) (#104)

A document could open any address with no confirmation: a link click, a
push button's URI action, a one-line button script mapped to a URI action,
or app.launchURL from JavaScript (on by default). That allowed sending
form data to an outside site, or handing file:, smb: and app-handler
addresses to the operating system.

All four now go through PdfCraftApp::request_document_url:

- engine::links::document_url accepts only http, https and mailto, up to
  2048 characters, and refuses control, zero-width, bidi-override and
  non-space whitespace characters, malformed schemes and web addresses
  without a host.
- Accepted addresses wait in an "Open this web page?" dialog that shows
  the real host (engine::links::host drops user:password@, and treats \
  like / as browsers do, so https://a.example\@b.example shows a.example)
  and the full address. Cancel is the highlighted button; Escape and
  clicking outside cancel. While one request waits, further ones are
  dropped, so a script can't stack dialogs.
- Refused addresses are reported in a notice and never opened.

PdfCraft's own links (Help, About, updates) still use open_url.
Preferences > JavaScript now says scripts need permission to open a web
page. Tests: engine unit tests for the policy and host parsing, and
crates/ui-egui/tests/document_links.rs driving the link, button, button
script, app.launchURL and Escape paths (all fail against the old code).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jcl-bit
2026-10-07 21:12:36 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 70da0179a6
commit cc7694be99
7 changed files with 474 additions and 17 deletions
+4 -4
View File
@@ -2202,8 +2202,8 @@ milestone = "M7"
status = "shipped"
commands = ["edit.link"]
tools = ["link_add"]
evidence = ["crates/annot/src/tests.rs::links_are_added_edited_listed_and_removed", "crates/automation/tests/automation.rs::links_through_tools"]
notes = ""
evidence = ["crates/annot/src/tests.rs::links_are_added_edited_listed_and_removed", "crates/automation/tests/automation.rs::links_through_tools", "crates/ui-egui/tests/document_links.rs::a_link_asks_first_and_opens_when_allowed", "crates/ui-egui/tests/document_links.rs::a_button_uri_action_asks_first"]
notes = "Opening a document's link or button URI asks first; only http, https and mailto addresses are offered (#90, #91)."
[[feature]]
id = "edit.action-launch"
@@ -5094,8 +5094,8 @@ milestone = "M6"
status = "shipped"
commands = ["app.preferences"]
tools = ["js_enabled"]
evidence = ["crates/js/src/tests.rs::the_sandbox_has_no_host_access", "crates/js/src/tests.rs::errors_and_runaway_scripts_are_contained", "crates/ui-egui/tests/javascript.rs::document_scripts_and_preferences"]
notes = "Preferences \u25b8 JavaScript \u25b8 Enable Acrobat JavaScript (remembered); no host access; loop and recursion limits."
evidence = ["crates/js/src/tests.rs::the_sandbox_has_no_host_access", "crates/js/src/tests.rs::errors_and_runaway_scripts_are_contained", "crates/ui-egui/tests/javascript.rs::document_scripts_and_preferences", "crates/ui-egui/tests/document_links.rs::a_script_asks_first_and_only_once", "crates/ui-egui/tests/document_links.rs::a_script_cannot_open_other_kinds_of_address"]
notes = "Preferences \u25b8 JavaScript \u25b8 Enable Acrobat JavaScript (remembered); no host access; loop and recursion limits; app.launchURL asks before opening and only offers http, https and mailto addresses (#90, #91)."
[[feature]]
id = "form.folder-level-js"