Skip to content
Merged
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
5 changes: 3 additions & 2 deletions src/components/BaseHead.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ interface Props {
image?: string;
}

import { withBase } from "../lib/utils";
const { title, description = "The official blog of the Python core development team.", image } = Astro.props;
import { withBase, stripDescriptionLinks } from "../lib/utils";
const { title, description: rawDescription = "The official blog of the Python core development team.", image } = Astro.props;
const description = stripDescriptionLinks(rawDescription);
const baseUrl = import.meta.env.DEV ? Astro.url.origin : Astro.site;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const ogImage = image ? new URL(image, baseUrl) : new URL(withBase("/og-default.png"), baseUrl);
Expand Down
4 changes: 2 additions & 2 deletions src/components/BlogPostCard.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
import { formatDate, postUrl, slugify, withBase } from "../lib/utils";
import { formatDate, postUrl, slugify, withBase, renderDescriptionLinks } from "../lib/utils";

interface Props {
slug: string;
Expand Down Expand Up @@ -29,7 +29,7 @@ const { slug, title, publishDate, author, description, tags, showEditLink = true
<time datetime={publishDate}>{formatDate(publishDate)}</time>
</div>
{description && (
<p class="mt-1.5 line-clamp-2 text-sm leading-relaxed text-zinc-500 dark:text-zinc-400">{description}</p>
<p class="mt-1.5 line-clamp-2 text-sm leading-relaxed text-zinc-500 dark:text-zinc-400" set:html={renderDescriptionLinks(description)} />
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description prop is rendered via set:html={renderDescriptionLinks(description)}, which outputs raw HTML built from unescaped description text. Because renderDescriptionLinks simply interpolates regex capture groups into an <a href="..."> template, malicious descriptions can inject arbitrary attributes, javascript: URLs, or HTML tags and trigger XSS when the card is viewed or clicked. Use a safe markdown/HTML rendering pipeline that escapes or strips dangerous tags/URL schemes instead of directly injecting the description via set:html.

Copilot uses AI. Check for mistakes.
)}
{tags && tags.length > 0 && (
<div class="mt-2.5 flex flex-wrap gap-1.5">
Expand Down
17 changes: 17 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ export function postUrl(slug: string, publishDate: string | Date): string {
return `/${year}/${month}/${slug}`;
}

/**
* Converts markdown-style links [text](url) in a description to HTML anchor tags.
*/
export function renderDescriptionLinks(desc: string): string {
return desc.replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
'<a href="$2" class="text-amber-700 underline hover:text-amber-900 dark:text-amber-400 dark:hover:text-amber-300">$1</a>',
);
}

/**
* Strips markdown-style links [text](url) to plain text for meta tags.
*/
export function stripDescriptionLinks(desc: string): string {
return desc.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
}

export function slugify(text: string): string {
return text
.normalize("NFD")
Expand Down
8 changes: 3 additions & 5 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const prerender = true;

import BaseLayout from "../layouts/BaseLayout.astro";
import PythonLogo from "../components/PythonLogo.astro";
import { formatDate, postUrl, withBase } from "../lib/utils";
import { formatDate, postUrl, withBase, renderDescriptionLinks } from "../lib/utils";
import { getCollection } from "astro:content";

const allPosts = await getCollection("posts");
Expand Down Expand Up @@ -66,9 +66,7 @@ const authors = new Set(posts.map((p) => p.data.author));
</div>

{featured.data.description && (
<p class="mt-3 max-w-2xl text-base leading-relaxed text-zinc-600 dark:text-zinc-400 sm:text-lg">
{featured.data.description}
</p>
<p class="mt-3 max-w-2xl text-base leading-relaxed text-zinc-600 dark:text-zinc-400 sm:text-lg" set:html={renderDescriptionLinks(featured.data.description)} />
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using set:html with renderDescriptionLinks(featured.data.description) renders the full description string as raw HTML without any sanitization or escaping. An attacker who can control featured.data.description (e.g., via CMS/content files) can inject arbitrary HTML/JS, including <script> tags or javascript:/attribute-based payloads, leading to XSS in visitors' browsers. To mitigate this, ensure descriptions are safely sanitized/escaped before passing to set:html, or use a markdown/HTML renderer that enforces a strict allowlist of safe tags and URL schemes.

Copilot uses AI. Check for mistakes.
)}
</a>

Expand Down Expand Up @@ -132,7 +130,7 @@ const authors = new Set(posts.map((p) => p.data.author));
<time datetime={post.data.publishDate.toISOString()}>{formatDate(post.data.publishDate.toISOString())}</time>
</div>
{post.data.description && (
<p class="mt-2 line-clamp-2 text-sm leading-relaxed text-zinc-500 dark:text-zinc-400">{post.data.description}</p>
<p class="mt-2 line-clamp-2 text-sm leading-relaxed text-zinc-500 dark:text-zinc-400" set:html={renderDescriptionLinks(post.data.description)} />
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using set:html with renderDescriptionLinks(post.data.description) injects description directly into the DOM as HTML, and renderDescriptionLinks only performs a regex replacement without escaping or sanitization. If post.data.description contains attacker-controlled HTML or crafted markdown links (e.g. URLs with quotes or javascript:), this can produce executable script or event handlers and result in XSS. Descriptions should be passed through a robust HTML/markdown sanitizer or safely escaped instead of being rendered directly with set:html.

Copilot uses AI. Check for mistakes.
)}
</article>
))}
Expand Down