Skip to content

Commit 1b82d83

Browse files
fix: disallow non-HTTP(S) protocols in links
1 parent 7b1f778 commit 1b82d83

File tree

3 files changed

+53
-3
lines changed

3 files changed

+53
-3
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openrouter/markdown-wasm",
3-
"version": "0.2.2",
3+
"version": "0.3.2",
44
"license": "Apache-2.0",
55
"packageManager": "[email protected]+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af",
66
"type": "module",

src/index.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { markdownToHtml } from "../dist";
44

55
test("adds attributes to links", async () => {
66
const html = await markdownToHtml("[link](/here)", {
7+
allowInternalLinks: true,
78
linkAttributes: { class: "someclass" },
89
});
910

@@ -23,6 +24,7 @@ test("adds extra attributes to external links", async () => {
2324

2425
test("doesn't add extra attributes to internal links", async () => {
2526
const html = await markdownToHtml("[link](/here)", {
27+
allowInternalLinks: true,
2628
linkAttributes: { class: "someclass" },
2729
externalLinkAttributes: { rel: "noopener noreferrer" },
2830
});
@@ -44,6 +46,7 @@ test("adds icon to external links", async () => {
4446

4547
test("doesn't add icon to internal links", async () => {
4648
const html = await markdownToHtml("[link](/here)", {
49+
allowInternalLinks: true,
4750
linkAttributes: { class: "someclass" },
4851
externalLinkAttributes: { rel: "noopener noreferrer" },
4952
externalLinkIconHtml: "<svg></svg>",
@@ -75,3 +78,33 @@ test("aborts on img tags", async () => {
7578

7679
expect(html).toBe(null);
7780
});
81+
82+
test("aborts on javascript: links in markdown", async () => {
83+
const html = await markdownToHtml(`[evil](javascript:alert(self))`, { allowInternalLinks: true });
84+
85+
expect(html).toBe(null);
86+
});
87+
88+
test("aborts on javascript: links in <a> tags", async () => {
89+
const html = await markdownToHtml(`<a href="javascript:alert('hello')">evil</a>`, { allowInternalLinks: true });
90+
91+
expect(html).toBe(null);
92+
});
93+
94+
test("denys internal links by default", async () => {
95+
const html = await markdownToHtml("[link](/here)", {
96+
linkAttributes: { class: "someclass" },
97+
});
98+
99+
expect(html).toBe(null)
100+
});
101+
102+
test("denys internal links if requested", async () => {
103+
const html = await markdownToHtml("[link](/here)", {
104+
allowInternalLinks: false,
105+
linkAttributes: { class: "someclass" },
106+
});
107+
108+
expect(html).toBe(null)
109+
});
110+

src/lib.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ use regex_lite::Regex;
55
use serde::{Deserialize, Serialize};
66
use std::collections::BTreeMap;
77
use tsify::Tsify;
8-
use wasm_bindgen::{convert::IntoWasmAbi, describe::WasmDescribe, prelude::*};
8+
use wasm_bindgen::{prelude::*};
99

1010
#[derive(Tsify, Serialize, Deserialize, Default)]
1111
#[serde(rename_all = "camelCase")]
1212
#[tsify(from_wasm_abi)]
1313
pub struct MarkdownRenderOptions {
14+
#[serde(default)]
15+
pub allow_internal_links: bool,
16+
1417
#[serde(default)]
1518
#[tsify(type = "Record<string, string>")]
1619
pub link_attributes: BTreeMap<String, String>,
@@ -181,7 +184,17 @@ impl FilteringIterator<'_> {
181184
where
182185
O::Error: Default,
183186
{
184-
let is_external = dest_url.starts_with("http");
187+
let is_external = dest_url.starts_with("http://") || dest_url.starts_with("https://");
188+
189+
let has_protocol_scheme = HAS_PROTOCOL_SCHEME_REGEX.is_match(dest_url);
190+
if has_protocol_scheme && !is_external {
191+
// Disallow `javascript:`, `tel:` etc. as well as custom protocol schemes
192+
return Err(O::Error::default());
193+
}
194+
195+
if !is_external && !self.options.allow_internal_links {
196+
return Err(O::Error::default());
197+
}
185198

186199
write!(output, r#"<a href=""#)?;
187200
if let Err(_) = escape_href(&mut *output, &dest_url) {
@@ -239,3 +252,7 @@ lazy_static! {
239252
r#"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)"#
240253
).unwrap();
241254
}
255+
256+
lazy_static! {
257+
static ref HAS_PROTOCOL_SCHEME_REGEX: Regex = Regex::new(r#"^(.*)\:"#).unwrap();
258+
}

0 commit comments

Comments
 (0)