Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion website/build/tsconfig.tsbuildinfo

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion website/src/components/layout/site/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
MAX_CONTENT_WIDTH,
THEME_COLORS,
} from "@/style";
import { sanitizeImageSrc } from "@/utils/url-helpers";

// Icons
import AngleLeftIconSvg from "@/images/icons/angle-left.svg";
Expand Down Expand Up @@ -487,7 +488,7 @@
{latestBlogPost.featuredImage && (
<TeaserImage>
<img
src={latestBlogPost.featuredImage}
src={sanitizeImageSrc(latestBlogPost.featuredImage)}

Check failure

Code scanning / CodeQL

Stored cross-site scripting High

Stored cross-site scripting vulnerability due to
stored value
.
alt={latestBlogPost.title}
Comment on lines 488 to 492
width={320}
height={180}
Expand Down
14 changes: 8 additions & 6 deletions website/src/components/misc/link.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import NextLink from "next/link";
import React, { FC } from "react";
import { sanitizeUrl } from "@/utils/url-helpers";

export const Link: FC<{
className?: string;
Expand All @@ -9,25 +10,26 @@
prefetch?: false;
children?: React.ReactNode;
}> = ({ to, prefetch, children, ...rest }) => {
const isHash = to.startsWith("#");
const internal = isHash || /^\/(?!\/)/.test(to);
const safeUrl = sanitizeUrl(to);
const isHash = safeUrl.startsWith("#");
const internal = isHash || /^\/(?!\/)/.test(safeUrl);

return isHash ? (
<a href={to} {...rest}>
<a href={safeUrl} {...rest}>

Check failure

Code scanning / CodeQL

Stored cross-site scripting High

Stored cross-site scripting vulnerability due to
stored value
.
{children}
</a>
) : internal ? (
prefetch === false ? (
<a href={to} {...rest}>
<a href={safeUrl} {...rest}>

Check failure

Code scanning / CodeQL

Stored cross-site scripting High

Stored cross-site scripting vulnerability due to
stored value
.
{children}
</a>
) : (
<NextLink href={to} {...rest}>
<NextLink href={safeUrl} {...rest}>

Check failure

Code scanning / CodeQL

Stored cross-site scripting High

Stored cross-site scripting vulnerability due to
stored value
.
{children}
</NextLink>
)
) : (
<a href={to} target="_blank" rel="noopener noreferrer" {...rest}>
<a href={safeUrl} target="_blank" rel="noopener noreferrer" {...rest}>

Check failure

Code scanning / CodeQL

Stored cross-site scripting High

Stored cross-site scripting vulnerability due to
stored value
.
{children}
</a>
);
Expand Down
60 changes: 60 additions & 0 deletions website/src/utils/url-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,66 @@
* Utility functions for working with URL parameters
*/

const SAFE_URL_PROTOCOLS = new Set(["http:", "https:", "mailto:", "tel:"]);

const SAFE_IMG_PROTOCOLS = new Set(["http:", "https:"]);

/**
* Sanitizes a URL to prevent XSS via dangerous protocols such as
* `javascript:` or `data:`. Returns the original URL when the protocol
* is safe, or "about:blank" otherwise. Relative and hash-only URLs are
* always allowed.
*/
export function sanitizeUrl(url: string): string {
const trimmed = url.trim();

// Relative paths and hash links are safe.
if (
trimmed === "" ||
trimmed.startsWith("/") ||
trimmed.startsWith("#") ||
trimmed.startsWith("?")
) {
return trimmed;
}

try {
const parsed = new URL(trimmed, "http://placeholder.invalid");

if (SAFE_URL_PROTOCOLS.has(parsed.protocol)) {
return trimmed;
}
} catch {
// Malformed URL — reject.
}

return "about:blank";
}

/**
* Sanitizes an image `src` value, allowing only http(s) and relative
* paths. Returns an empty string for anything else.
*/
export function sanitizeImageSrc(src: string): string {
const trimmed = src.trim();

if (trimmed === "" || trimmed.startsWith("/")) {
return trimmed;
}

try {
const parsed = new URL(trimmed, "http://placeholder.invalid");

if (SAFE_IMG_PROTOCOLS.has(parsed.protocol)) {
return trimmed;
}
} catch {
// Malformed URL — reject.
}

return "";
}
Comment on lines +45 to +63

/**
* Gets a query parameter value from the current URL
* @param paramName - The name of the parameter to retrieve
Expand Down
Loading