mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-19 13:12:48 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3e0f4c6da |
@@ -218,6 +218,50 @@ test.describe("Chat — streaming @medium", () => {
|
||||
frigateApp.page.getByText(/searching for people/i),
|
||||
).toBeVisible({ timeout: 5_000 });
|
||||
});
|
||||
|
||||
test("assistant markdown renders only same-origin images", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
const reply =
|
||||
" " +
|
||||
" " +
|
||||
"[offsite](https://evil.example/page)";
|
||||
await installChatStreamOverride(frigateApp, [
|
||||
{ type: "content", delta: reply },
|
||||
{
|
||||
type: "messages",
|
||||
messages: [
|
||||
{ role: "system", content: "sys" },
|
||||
{ role: "user", content: "show me" },
|
||||
{ role: "assistant", content: reply },
|
||||
],
|
||||
},
|
||||
{ type: "done" },
|
||||
]);
|
||||
const offsiteRequests: string[] = [];
|
||||
frigateApp.page.on("request", (req) => {
|
||||
if (req.url().includes("evil.example")) offsiteRequests.push(req.url());
|
||||
});
|
||||
await frigateApp.goto("/chat");
|
||||
const input = frigateApp.page.getByPlaceholder(/ask/i);
|
||||
await expect(input).toBeVisible({ timeout: 10_000 });
|
||||
await input.fill("show me");
|
||||
await input.press("Enter");
|
||||
|
||||
await expect(frigateApp.page.getByText("leaked alt")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(
|
||||
frigateApp.page.locator('img[src*="evil.example"]'),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
frigateApp.page.locator('img[src="/api/events/abc/thumbnail.webp"]'),
|
||||
).toHaveCount(1);
|
||||
const link = frigateApp.page.getByRole("link", { name: "offsite" });
|
||||
await expect(link).toHaveAttribute("target", "_blank");
|
||||
await expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
||||
expect(offsiteRequests).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Chat — stop @medium", () => {
|
||||
|
||||
@@ -18,6 +18,18 @@ import { cn } from "@/lib/utils";
|
||||
import { ChatAttachmentChip } from "@/components/chat/ChatAttachmentChip";
|
||||
import { parseAttachedEvent } from "@/utils/chatUtil";
|
||||
import type { ChatStats, ShowStatsMode } from "@/types/chat";
|
||||
import { baseUrl } from "@/api/baseUrl";
|
||||
|
||||
// Assistant output can carry prompt-injected URLs, and the browser fetches
|
||||
// images without a click, so only same-origin images render.
|
||||
function isSameOrigin(url: string | undefined): boolean {
|
||||
if (!url) return false;
|
||||
try {
|
||||
return new URL(url, baseUrl).origin === window.location.origin;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
type MessageBubbleProps = {
|
||||
role: "user" | "assistant";
|
||||
@@ -223,6 +235,23 @@ export function MessageBubble({
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
img: ({ node: _n, src, alt, ...props }) =>
|
||||
typeof src === "string" && isSameOrigin(src) ? (
|
||||
<img src={src} alt={alt} {...props} />
|
||||
) : (
|
||||
<>{alt}</>
|
||||
),
|
||||
a: ({ node: _n, href, ...props }) =>
|
||||
isSameOrigin(href) ? (
|
||||
<a href={href} {...props} />
|
||||
) : (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
|
||||
Reference in New Issue
Block a user