mirror of
https://github.com/multipleof4/ntfy.git
synced 2026-10-12 14:55:22 +00:00
Strip unsafe URLs from Markdown links
This commit is contained in:
@@ -444,6 +444,17 @@ export const updateFavicon = async (count) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Matches a URL whose scheme is NOT in the safe list (https, mailto, ...). A scheme
|
||||
// (RFC 3986: [a-z][a-z0-9+.-]*) can't contain a slash, query, or hash, so a colon later
|
||||
// in the path/query (e.g. foo?x=a:b) is never mistaken for a protocol separator. Relative
|
||||
// URLs have no scheme and never match. This strips javascript:/data:/vbscript:/... so React
|
||||
// never has to block a javascript: URL at render time -- which it does loudly, throwing an
|
||||
// uncaught error.
|
||||
const unsafeUrlProtocol = /^\s*(?!(?:https?|ftps?|ircs?|mailto|xmpp|tel):)[a-z][a-z0-9+.-]*:/i;
|
||||
|
||||
// Returns the URL unchanged if it uses a safe protocol or is relative, otherwise "".
|
||||
export const sanitizeUrl = (url) => (url && unsafeUrlProtocol.test(url) ? "" : url || "");
|
||||
|
||||
export const copyToClipboard = (text) => {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
return navigator.clipboard.writeText(text);
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
withBearerAuth,
|
||||
maybeWithAuth,
|
||||
splitNoEmpty,
|
||||
sanitizeUrl,
|
||||
hashCode,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
@@ -216,3 +217,40 @@ describe("date/time formatting", () => {
|
||||
expect(formatDateTime(ts, DATE_FORMAT.ISO8601, TIME_FORMAT.H12)).toBe("2026-03-08 14:30");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sanitizeUrl", () => {
|
||||
it("keeps safe absolute URLs", () => {
|
||||
expect(sanitizeUrl("https://ntfy.sh")).toBe("https://ntfy.sh");
|
||||
expect(sanitizeUrl("http://example.com/foo?bar=1")).toBe("http://example.com/foo?bar=1");
|
||||
expect(sanitizeUrl("mailto:phil@ntfy.sh")).toBe("mailto:phil@ntfy.sh");
|
||||
expect(sanitizeUrl("ftp://ftp.example.com/file.txt")).toBe("ftp://ftp.example.com/file.txt");
|
||||
expect(sanitizeUrl("ftps://ftp.example.com/file.txt")).toBe("ftps://ftp.example.com/file.txt");
|
||||
});
|
||||
|
||||
it("keeps relative URLs, fragments and query strings", () => {
|
||||
expect(sanitizeUrl("/docs")).toBe("/docs");
|
||||
expect(sanitizeUrl("./foo")).toBe("./foo");
|
||||
expect(sanitizeUrl("#section")).toBe("#section");
|
||||
expect(sanitizeUrl("?q=1")).toBe("?q=1");
|
||||
expect(sanitizeUrl("foo/bar")).toBe("foo/bar");
|
||||
});
|
||||
|
||||
it("does not treat a colon after a slash/query/hash as a protocol", () => {
|
||||
expect(sanitizeUrl("/path:with:colons")).toBe("/path:with:colons");
|
||||
expect(sanitizeUrl("foo?x=a:b")).toBe("foo?x=a:b");
|
||||
});
|
||||
|
||||
it("strips dangerous protocols", () => {
|
||||
expect(sanitizeUrl("javascript:alert(document.domain)")).toBe("");
|
||||
expect(sanitizeUrl("JavaScript:alert(1)")).toBe("");
|
||||
expect(sanitizeUrl(" javascript:alert(1)")).toBe("");
|
||||
expect(sanitizeUrl("vbscript:msgbox(1)")).toBe("");
|
||||
expect(sanitizeUrl("data:text/html,<script>alert(1)</script>")).toBe("");
|
||||
});
|
||||
|
||||
it("handles empty and nullish input", () => {
|
||||
expect(sanitizeUrl("")).toBe("");
|
||||
expect(sanitizeUrl(undefined)).toBe("");
|
||||
expect(sanitizeUrl(null)).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user