diff --git a/CHANGELOG.md b/CHANGELOG.md index 16e0c49c..401aaed6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Ecosystem: promote clawpdf into the TypeScript libraries section with its canonical site link. +- Website: sanitize data-driven external links before rendering them into `href` attributes (#143, thanks @SebTardif). - Website: move the Discord shortcut into Vercel routing so `/discord` redirects correctly (#147, thanks @SebTardif). - Ecosystem: tune project card banner art visibility. - Ecosystem: quiet the final contribution CTA button colors. diff --git a/package.json b/package.json index c4647173..6109476a 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "astro dev", "build": "astro build", + "test": "bun test", "avatars:cache": "node scripts/cache-avatars.mjs", "preview": "astro preview" }, diff --git a/src/lib/sanitize-url.ts b/src/lib/sanitize-url.ts new file mode 100644 index 00000000..a8ef2211 --- /dev/null +++ b/src/lib/sanitize-url.ts @@ -0,0 +1,21 @@ +const allowedProtocols = new Set(['http:', 'https:', 'mailto:']); + +function normalizeForProtocolCheck(url: string): string { + let decoded = url; + try { + decoded = decodeURIComponent(url); + } catch { + // Malformed escapes still flow through URL parsing below. + } + + return decoded.replace(/[\x00-\x1f\x7f]/g, ''); +} + +export function sanitizeUrl(url: string): string { + try { + const parsed = new URL(normalizeForProtocolCheck(url), 'https://openclaw.ai'); + return allowedProtocols.has(parsed.protocol) ? url : '#'; + } catch { + return '#'; + } +} diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro index 8ac41da8..d771d3ae 100644 --- a/src/pages/blog/[...slug].astro +++ b/src/pages/blog/[...slug].astro @@ -4,6 +4,7 @@ import SiteTopbar from '../../components/SiteTopbar.astro'; import { render } from 'astro:content'; import { getPublishedBlogPosts } from '../../lib/blog'; import { getCachedXAvatarSrc, getInitialsAvatarSrc } from '../../lib/avatars'; +import { sanitizeUrl } from '../../lib/sanitize-url'; export async function getStaticPaths() { const posts = await getPublishedBlogPosts(); @@ -127,7 +128,7 @@ const postUrl = `https://openclaw.ai/blog/${post.id}`; {getAuthorLinks(author).length > 0 && ( )} diff --git a/src/pages/index.astro b/src/pages/index.astro index c1b9ad10..38dfe3b1 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -8,6 +8,7 @@ import communityBuilds from '../data/community-builds.json'; import pressArticles from '../data/press.json'; import { getPublishedBlogPosts } from '../lib/blog'; import { getCachedXAvatarSrc, getInitialsAvatarSrc } from '../lib/avatars'; +import { sanitizeUrl } from '../lib/sanitize-url'; const blogPosts = await getPublishedBlogPosts(); const [latestPost] = blogPosts; @@ -534,7 +535,7 @@ function formatBlogDate(date: Date): string {
{row1.map((t) => ( - + {t.author}
{row2.map((t) => ( - + {t.author}
{featuredBuilds.map((build) => ( - +
{build.source} Open → @@ -694,7 +695,7 @@ function formatBlogDate(date: Date): string {