Skip to content

JSON-LD script inlined without </script> escape on research page (XSS) #979

@MaxGhenis

Description

@MaxGhenis

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecuritySecurity-related issue

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions