Skip to content

Commit 5706fbd

Browse files
__staticRouterHydrationData html tag escape (#10068)
Co-authored-by: Matt Brophy <matt@brophy.org>
1 parent 1d00b40 commit 5706fbd

File tree

4 files changed

+54
-1
lines changed

4 files changed

+54
-1
lines changed

.changeset/witty-melons-sip.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": patch
3+
---
4+
5+
Properly escape HTML characters in StaticRouterProvider serialized hydration data

contributors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,4 @@
178178
- xavier-lc
179179
- xcsnowcity
180180
- yuleicul
181+
- zheng-chuang

packages/react-router-dom/__tests__/data-static-router-test.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,37 @@ describe("A <StaticRouterProvider>", () => {
269269
);
270270
});
271271

272+
it("escapes HTML tags in serialized hydration data", async () => {
273+
let routes = [
274+
{
275+
path: "/",
276+
loader: () => ({
277+
key: "uh </script> oh",
278+
}),
279+
element: <h1>👋</h1>,
280+
},
281+
];
282+
let { query } = createStaticHandler(routes);
283+
284+
let context = (await query(
285+
new Request("http://localhost/", {
286+
signal: new AbortController().signal,
287+
})
288+
)) as StaticHandlerContext;
289+
290+
let html = ReactDOMServer.renderToStaticMarkup(
291+
<React.StrictMode>
292+
<StaticRouterProvider
293+
router={createStaticRouter(routes, context)}
294+
context={context}
295+
/>
296+
</React.StrictMode>
297+
);
298+
expect(html).toMatchInlineSnapshot(
299+
`"<h1>👋</h1><script>window.__staticRouterHydrationData = JSON.parse("{\\"loaderData\\":{\\"0\\":{\\"key\\":\\"uh \\u003c/script\\u003e oh\\"}},\\"actionData\\":null,\\"errors\\":null}");</script>"`
300+
);
301+
});
302+
272303
it("serializes ErrorResponse instances", async () => {
273304
let routes = [
274305
{

packages/react-router-dom/server.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export function StaticRouterProvider({
114114
// up parsing on the client. Dual-stringify is needed to ensure all quotes
115115
// are properly escaped in the resulting string. See:
116116
// https://v8.dev/blog/cost-of-javascript-2019#json
117-
let json = JSON.stringify(JSON.stringify(data));
117+
let json = htmlEscape(JSON.stringify(JSON.stringify(data)));
118118
hydrateScript = `window.__staticRouterHydrationData = JSON.parse(${json});`;
119119
}
120120

@@ -323,3 +323,19 @@ function encodeLocation(to: To): Path {
323323
hash: path.hash || "",
324324
};
325325
}
326+
327+
// This utility is based on https://github.com/zertosh/htmlescape
328+
// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE
329+
const ESCAPE_LOOKUP: { [match: string]: string } = {
330+
"&": "\\u0026",
331+
">": "\\u003e",
332+
"<": "\\u003c",
333+
"\u2028": "\\u2028",
334+
"\u2029": "\\u2029",
335+
};
336+
337+
const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
338+
339+
function htmlEscape(str: string): string {
340+
return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
341+
}

0 commit comments

Comments
 (0)