fix: secure widget navigation links

This commit is contained in:
Thomas Camlong committed 2026-08-02 10:01:36 +02:00
1 parent 1d73513989
commit d460ac8bbb
3 files changed
+30 -6

No files matched your search

+16 -3
View File
@@ -69,7 +69,7 @@ export default function RssFeed({ options, width, displayMode }: WidgetComponent
<Card
key={feedEntry.id}
component={"a"}
href={getSafeExternalUrl(feedEntry.link, feedEntry.feedUrl) ?? getSafeExternalUrl(feedEntry.feedUrl)}
href={getSafeExternalUrl(feedEntry.link, feedEntry.feedUrl)}
radius={board.itemRadius}
target="_blank"
rel="noopener noreferrer"
@@ -141,8 +141,21 @@ export const getHostname = (url: string): string => {
export const getSafeExternalUrl = (value: unknown, baseUrl?: string): string | undefined => {
if (typeof value !== "string") return undefined;
try {
const url = new URL(value, baseUrl);
return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : undefined;
let sanitizedBaseUrl: string | undefined;
if (baseUrl) {
const parsedBaseUrl = new URL(baseUrl);
parsedBaseUrl.username = "";
parsedBaseUrl.password = "";
parsedBaseUrl.search = "";
parsedBaseUrl.hash = "";
sanitizedBaseUrl = parsedBaseUrl.toString();
}
const url = new URL(value, sanitizedBaseUrl);
if (url.protocol !== "http:" && url.protocol !== "https:") return undefined;
url.username = "";
url.password = "";
return url.toString();
} catch {
return undefined;
}
@@ -14,9 +14,20 @@ describe("RSS feed display helpers", () => {
expect(getHostname("https://example.com/feed.xml")).toBe("example.com");
});
test("only exposes HTTP links from untrusted feed entries", () => {
test("only exposes credential-free HTTP links from untrusted feed entries", () => {
expect(getSafeExternalUrl("https://example.com/story")).toBe("https://example.com/story");
expect(getSafeExternalUrl("/story", "https://example.com/feed.xml")).toBe("https://example.com/story");
expect(getSafeExternalUrl("/story", "https://user:password@example.com/feed.xml")).toBe(
"https://example.com/story",
);
expect(getSafeExternalUrl("https://user:password@example.com/story")).toBe("https://example.com/story");
expect(getSafeExternalUrl(undefined, "https://example.com/feed?token=secret")).toBeUndefined();
expect(getSafeExternalUrl("#story", "https://example.com/feed?token=secret#old")).toBe(
"https://example.com/feed#story",
);
expect(getSafeExternalUrl("?story=1", "https://example.com/feed?token=secret")).toBe(
"https://example.com/feed?story=1",
);
expect(getSafeExternalUrl("javascript:alert(1)")).toBeUndefined();
expect(getSafeExternalUrl("not a URL")).toBeUndefined();
});