mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-06 02:48:25 +00:00
fix(web): sanitize markdown links in run stages
Reject unsafe markdown hrefs and strip raw HTML so stage output cannot smuggle protocol-relative or scripted links into the run detail UI.
This commit is contained in:
parent
e92a2ba9db
commit
db01b9634b
2 changed files with 50 additions and 2 deletions
17
apps/fabro-web/app/routes/run-stages.test.ts
Normal file
17
apps/fabro-web/app/routes/run-stages.test.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { isSafeMarkdownHref } from "./run-stages";
|
||||
|
||||
describe("isSafeMarkdownHref", () => {
|
||||
test("rejects protocol-relative URLs", () => {
|
||||
expect(isSafeMarkdownHref("//attacker.example/pixel.png")).toBe(false);
|
||||
});
|
||||
|
||||
test("accepts root-relative, hash, http, https, and mailto URLs", () => {
|
||||
expect(isSafeMarkdownHref("/runs/run-1")).toBe(true);
|
||||
expect(isSafeMarkdownHref("#section-1")).toBe(true);
|
||||
expect(isSafeMarkdownHref("https://fabro.sh")).toBe(true);
|
||||
expect(isSafeMarkdownHref("http://localhost:3000")).toBe(true);
|
||||
expect(isSafeMarkdownHref("mailto:test@example.com")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,37 @@
|
|||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import { marked } from "marked";
|
||||
import { Marked } from "marked";
|
||||
|
||||
const SAFE_HTTP_URL_RE = /^https?:\/\//i;
|
||||
const SAFE_MAILTO_URL_RE = /^mailto:/i;
|
||||
|
||||
export function isSafeMarkdownHref(href: string): boolean {
|
||||
return (
|
||||
SAFE_HTTP_URL_RE.test(href) ||
|
||||
SAFE_MAILTO_URL_RE.test(href) ||
|
||||
href.startsWith("#") ||
|
||||
(href.startsWith("/") && !href.startsWith("//"))
|
||||
);
|
||||
}
|
||||
|
||||
const markedSafe = new Marked();
|
||||
markedSafe.use({
|
||||
async: false,
|
||||
walkTokens(token) {
|
||||
if (
|
||||
(token.type === "link" || token.type === "image") &&
|
||||
typeof token.href === "string" &&
|
||||
!isSafeMarkdownHref(token.href)
|
||||
) {
|
||||
token.href = "";
|
||||
}
|
||||
},
|
||||
renderer: {
|
||||
html() {
|
||||
return "";
|
||||
},
|
||||
},
|
||||
});
|
||||
import { CommandLineIcon, ChatBubbleLeftIcon, PlayIcon } from "@heroicons/react/24/outline";
|
||||
import { ToolBlock } from "../components/tool-use";
|
||||
import type { ToolUse } from "../components/tool-use";
|
||||
|
|
@ -167,7 +198,7 @@ export async function loader({ request, params }: any) {
|
|||
}
|
||||
|
||||
function Markdown({ content }: { content: string }) {
|
||||
const html = useMemo(() => marked.parse(content, { async: false }) as string, [content]);
|
||||
const html = useMemo(() => markedSafe.parse(content, { async: false }) as string, [content]);
|
||||
return (
|
||||
<div
|
||||
className="prose prose-sm max-w-none text-fg-3 prose-headings:text-fg-2 prose-strong:text-fg-2 prose-code:rounded prose-code:bg-overlay-strong prose-code:px-1 prose-code:py-0.5 prose-code:text-[0.8em] prose-code:font-mono prose-code:text-fg-3 prose-code:before:content-none prose-code:after:content-none prose-pre:bg-overlay-strong prose-pre:text-fg-3 prose-a:text-teal-500"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue