Summary
JSON-LD structured-data payload rendered via dangerouslySetInnerHTML on the Next.js research page is not escaped against a literal </script> in post content. Because the JSON is inlined without the standard </ → \u003c transform that middleware.ts already applies, a post title or description containing </script> terminates the JSON script block and permits execution of attacker-controlled HTML that follows it.
Location
website/src/app/[countryId]/research/[slug]/page.tsx:94-97
What goes wrong
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
jsonLd.headline and jsonLd.description come from post frontmatter. HTML parsers terminate a <script> element at the literal sequence </script> inside its text content, regardless of JSON-string quoting. Any post whose title/description contains </script><img src=x onerror=...> escapes the block and executes the trailing HTML in the page origin. The existing pre-render path in middleware.ts:238 already applies the mitigation:
<script type="application/ld+json">${JSON.stringify(jsonLd).replace(/</g, "\\u003c")}</script>
The Next.js research page lacks this escape.
Suggested fix
Apply the same escape utility to the JSON-LD payload:
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
Ideally extract a shared helper (e.g., encodeJsonForScript) and reuse it in both middleware.ts and every page.tsx that inlines JSON into <script>.
Severity
Critical (stored XSS) — reflected through any post whose frontmatter title/description contains </script>. Post content is author-controlled in principle but is parsed through markdown rendering so the blast radius is large.
Relates to
#949, #950, #956 (markdown/raw-HTML sanitization umbrella).
Summary
JSON-LD structured-data payload rendered via
dangerouslySetInnerHTMLon the Next.js research page is not escaped against a literal</script>in post content. Because the JSON is inlined without the standard</→\u003ctransform thatmiddleware.tsalready applies, a post title or description containing</script>terminates the JSON script block and permits execution of attacker-controlled HTML that follows it.Location
website/src/app/[countryId]/research/[slug]/page.tsx:94-97What goes wrong
jsonLd.headlineandjsonLd.descriptioncome from post frontmatter. HTML parsers terminate a<script>element at the literal sequence</script>inside its text content, regardless of JSON-string quoting. Any post whose title/description contains</script><img src=x onerror=...>escapes the block and executes the trailing HTML in the page origin. The existing pre-render path inmiddleware.ts:238already applies the mitigation:The Next.js research page lacks this escape.
Suggested fix
Apply the same escape utility to the JSON-LD payload:
Ideally extract a shared helper (e.g.,
encodeJsonForScript) and reuse it in bothmiddleware.tsand everypage.tsxthat inlines JSON into<script>.Severity
Critical (stored XSS) — reflected through any post whose frontmatter title/description contains
</script>. Post content is author-controlled in principle but is parsed through markdown rendering so the blast radius is large.Relates to
#949, #950, #956 (markdown/raw-HTML sanitization umbrella).