diff --git a/packages/widgets/src/rssFeed/component.tsx b/packages/widgets/src/rssFeed/component.tsx index 2fc2b9147..79a070420 100644 --- a/packages/widgets/src/rssFeed/component.tsx +++ b/packages/widgets/src/rssFeed/component.tsx @@ -69,7 +69,7 @@ export default function RssFeed({ options, width, displayMode }: WidgetComponent { 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; } diff --git a/packages/widgets/src/rssFeed/feed-display.spec.ts b/packages/widgets/src/rssFeed/feed-display.spec.ts index 3b0b460b9..3348ef91f 100644 --- a/packages/widgets/src/rssFeed/feed-display.spec.ts +++ b/packages/widgets/src/rssFeed/feed-display.spec.ts @@ -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(); }); diff --git a/packages/widgets/src/timetable/component.tsx b/packages/widgets/src/timetable/component.tsx index c82f8b2e2..7a50c3f70 100644 --- a/packages/widgets/src/timetable/component.tsx +++ b/packages/widgets/src/timetable/component.tsx @@ -60,8 +60,8 @@ const TimetableWidgetInner = ({ station, baseUrl, itemId, displayMode, width }: {t("title", { station: station.label })} {compactStaleWarning && displayMode === "compact" && ( - - + + {compactStaleWarning}