Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Closes #4420 Closes #4319
This commit is contained in:
1 parent
4a91b36c09
commit
f94508994f
4 files changed
+26
-6
No files matched your search
@@ -3,6 +3,12 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [v0.51.505] — 2026-06-18 — Release RP (allow macOS Mail `message:` links in chat markdown)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **`message:` links (macOS Mail message URLs) now render as clickable links in chat instead of being blocked (#4319).** The markdown renderer's link allowlists treated `message:` like an unknown scheme and dropped it; it's now allowed alongside the other OS-delegated schemes (`mailto:`/`tel:`) on every render path (streamed markdown + the two `renderMd` link passes + the anchor-safety check). The `javascript:`/`data:`/`vbscript:` denylist still fires first, so this adds no script-execution surface. Useful for RAG/notes workflows that link back to source emails. Thanks @bergeouss.
|
||||||
|
|
||||||
## [v0.51.504] — 2026-06-18 — Release RO (group OpenRouter / Nous models by vendor in the model picker)
|
## [v0.51.504] — 2026-06-18 — Release RO (group OpenRouter / Nous models by vendor in the model picker)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+1
-1
@@ -2219,7 +2219,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||||||
}
|
}
|
||||||
// Allowed URL schemes for anchors and images rendered from agent-streamed markdown.
|
// Allowed URL schemes for anchors and images rendered from agent-streamed markdown.
|
||||||
// Raw file:// anchors are rewritten to /api/media before the user can click them.
|
// Raw file:// anchors are rewritten to /api/media before the user can click them.
|
||||||
const _SMD_SAFE_URL_RE=/^(?:https?:|mailto:|tel:|\/|#|\?|\.|api|session\/)/i;
|
const _SMD_SAFE_URL_RE=/^(?:https?:|mailto:|tel:|message:|\/|#|\?|\.|api|session\/)/i;
|
||||||
const _SMD_SAFE_IMG_URL_RE=/^(?:https?:|mailto:|tel:|\/|#|\?|\.)/i;
|
const _SMD_SAFE_IMG_URL_RE=/^(?:https?:|mailto:|tel:|\/|#|\?|\.)/i;
|
||||||
function _smdLinkHref(raw){
|
function _smdLinkHref(raw){
|
||||||
const href=String(raw||'');
|
const href=String(raw||'');
|
||||||
|
|||||||
+3
-3
@@ -4692,7 +4692,7 @@ function renderMd(raw){
|
|||||||
t=t.replace(/\x00C(\d+)\x00/g,(_,i)=>_code_stash[+i]);
|
t=t.replace(/\x00C(\d+)\x00/g,(_,i)=>_code_stash[+i]);
|
||||||
// Stash [label](url) links before autolink so the URL in href= is not re-linked
|
// Stash [label](url) links before autolink so the URL in href= is not re-linked
|
||||||
const _link_stash=[];
|
const _link_stash=[];
|
||||||
t=t.replace(/\[([^\]]+)\]\(((?:https?:\/\/|file:\/\/|workspace:\/\/|session:\/\/|mailto:|tel:)[^\s\)]+)\)/g,(_,lb,u)=>{_link_stash.push(_markdownAnchor(lb,u));return `\x00L${_link_stash.length-1}\x00`;});
|
t=t.replace(/\[([^\]]+)\]\(((?:https?:\/\/|file:\/\/|workspace:\/\/|session:\/\/|mailto:|tel:|message:)[^\s\)]+)\)/g,(_,lb,u)=>{_link_stash.push(_markdownAnchor(lb,u));return `\x00L${_link_stash.length-1}\x00`;});
|
||||||
t=t.replace(/(https?:\/\/[^\s<>"')\]]+)/g,(url)=>{const trail=url.match(/[.,;:!?)]$/)?url.slice(-1):'';const clean=trail?url.slice(0,-1):url;return `<a href="${clean}" target="_blank" rel="noopener">${esc(clean)}</a>${trail}`;});
|
t=t.replace(/(https?:\/\/[^\s<>"')\]]+)/g,(url)=>{const trail=url.match(/[.,;:!?)]$/)?url.slice(-1):'';const clean=trail?url.slice(0,-1):url;return `<a href="${clean}" target="_blank" rel="noopener">${esc(clean)}</a>${trail}`;});
|
||||||
t=t.replace(/\x00L(\d+)\x00/g,(_,i)=>_link_stash[+i]);
|
t=t.replace(/\x00L(\d+)\x00/g,(_,i)=>_link_stash[+i]);
|
||||||
t=t.replace(/\x00G(\d+)\x00/g,(_,i)=>_img_stash[+i]);
|
t=t.replace(/\x00G(\d+)\x00/g,(_,i)=>_img_stash[+i]);
|
||||||
@@ -4833,7 +4833,7 @@ function renderMd(raw){
|
|||||||
// Stash existing <a> tags first to avoid re-linking already-linked URLs.
|
// Stash existing <a> tags first to avoid re-linking already-linked URLs.
|
||||||
const _a_stash=[];
|
const _a_stash=[];
|
||||||
s=s.replace(/(<a\b[^>]*>[\s\S]*?<\/a>)/g,m=>{_a_stash.push(m);return `\x00A${_a_stash.length-1}\x00`;});
|
s=s.replace(/(<a\b[^>]*>[\s\S]*?<\/a>)/g,m=>{_a_stash.push(m);return `\x00A${_a_stash.length-1}\x00`;});
|
||||||
s=s.replace(/\[([^\]]+)\]\(((?:https?:\/\/|file:\/\/|workspace:\/\/|session:\/\/|mailto:|tel:)[^\s\)]+)\)/g,(_,label,url)=>_markdownAnchor(label,url));
|
s=s.replace(/\[([^\]]+)\]\(((?:https?:\/\/|file:\/\/|workspace:\/\/|session:\/\/|mailto:|tel:|message:)[^\s\)]+)\)/g,(_,label,url)=>_markdownAnchor(label,url));
|
||||||
s=s.replace(/\x00A(\d+)\x00/g,(_,i)=>_a_stash[+i]);
|
s=s.replace(/\x00A(\d+)\x00/g,(_,i)=>_a_stash[+i]);
|
||||||
// Restore raw <pre> only after markdown rewrites so literal preformatted
|
// Restore raw <pre> only after markdown rewrites so literal preformatted
|
||||||
// content stays placeholder-protected, then let the sanitizer normalize tags.
|
// content stays placeholder-protected, then let the sanitizer normalize tags.
|
||||||
@@ -4919,7 +4919,7 @@ function renderMd(raw){
|
|||||||
if(!compact) return false;
|
if(!compact) return false;
|
||||||
if(/^(javascript|data|vbscript):/i.test(compact)) return false;
|
if(/^(javascript|data|vbscript):/i.test(compact)) return false;
|
||||||
if(/^https?:\/\//i.test(raw)) return true;
|
if(/^https?:\/\//i.test(raw)) return true;
|
||||||
if(/^(mailto:|tel:)/i.test(raw)) return true;
|
if(/^(mailto:|tel:|message:)/i.test(raw)) return true;
|
||||||
if(img && /^api\//i.test(raw)) return true;
|
if(img && /^api\//i.test(raw)) return true;
|
||||||
if(!img && (/^api\//i.test(raw) || /^#/.test(raw) || _isInternalSessionHref(raw))) return true;
|
if(!img && (/^api\//i.test(raw) || /^#/.test(raw) || _isInternalSessionHref(raw))) return true;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -63,10 +63,24 @@ def _render(markdown: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def test_workspace_markdown_renders_mailto_and_tel_links():
|
def test_workspace_markdown_renders_mailto_and_tel_links():
|
||||||
html = _render("[email](mailto:foo@example.test) and [phone](tel:+15551212)")
|
html = _render("[email](mailto:foo@example.test) and [phone](tel:+155****1212)")
|
||||||
|
|
||||||
assert '<a href="mailto:foo@example.test" target="_blank" rel="noopener">email</a>' in html
|
assert '<a href="mailto:foo@example.test" target="_blank" rel="noopener">email</a>' in html
|
||||||
assert '<a href="tel:+15551212" target="_blank" rel="noopener">phone</a>' in html
|
assert '<a href="tel:+155****1212" target="_blank" rel="noopener">phone</a>' in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_workspace_markdown_renders_message_scheme_links():
|
||||||
|
"""macOS Mail `message:` links must render clickable (#4319) — same OS-handled
|
||||||
|
class as mailto:/tel: — while `javascript:` stays blocked even when a
|
||||||
|
`message:`-style label is attached."""
|
||||||
|
html = _render(
|
||||||
|
"[mail](message://%3Cabc@example.test%3E) "
|
||||||
|
"and [x](javascript:alert(1))"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert '<a href="message://%3Cabc@example.test%3E" target="_blank" rel="noopener">mail</a>' in html
|
||||||
|
# The javascript: scheme must NOT produce an executable href.
|
||||||
|
assert "javascript:alert(1)" not in html or 'href="javascript:' not in html
|
||||||
|
|
||||||
|
|
||||||
def test_workspace_html_iframe_allows_links_to_escape_sandbox():
|
def test_workspace_html_iframe_allows_links_to_escape_sandbox():
|
||||||
|
|||||||
Reference in new issue
Block a user