Guide 105 worked guide

How to review cross-site scripting in a modern frontend

Review frontend XSS through input, rendering context, viewer permissions, and rich-text policy, with careful limits on what a harmless marker can demonstrate.

To review an XSS alert in a modern frontend, follow an attacker-influenced value to the actual browser output context. Ordinary React text children and textContent on an ordinary display element treat a string as text. Raw HTML APIs, framework escape hatches, URL attributes and script contexts need different controls. A framework's default escaping can rebut a claim on one path, but it does not protect every component, plugin or post-render DOM change. State which user can supply the value, who views it, and which sink finally receives it before you call a finding confirmed or false.

OWASP's XSS prevention guide explains why output encoding depends on the destination context. React's component documentation explicitly warns that dangerouslySetInnerHTML overrides the normal HTML rendering boundary. This is a code-review method, not a request to try script payloads against a live site.

Follow an illustrative activity note

Invented application and code; no browser exploit was performed. Cedar Reports shows an activity feed. A team member can write a note on a report, and other authorized members can view it. A scanner marks the note component as XSS because note.body comes from the API:

function NoteRow({ note }) {
  return <p className="note">{note.body}</p>;
}

On this React path, note.body is a string child, not an HTML string passed to a raw HTML sink. A reviewer can reject the specific claim that this line parses the note as HTML if the component and data shape are as shown. The conclusion still needs a pinned revision and confirmation that this is the rendered path. It does not cover a separate export view, a third-party widget, or a later ref that replaces the element's contents.

Now consider a preview component in the same illustrative application:

function NotePreview({ convertedHtml }) {
  return <div dangerouslySetInnerHTML={{ __html: convertedHtml }} />;
}

Here the browser parses convertedHtml as HTML. The review must trace where it came from: Is it fixed application markup, server-generated Markdown output, user-authored rich text, or a trusted sanitizer's result? The prop name “convertedHtml” says nothing about safety. If a team member's note is converted to HTML and then reaches this component without an appropriate sanitizer, there is a supported XSS concern. The reviewer should still confirm the actual conversion and viewer route before asserting that a harmful browser action is possible. React's documentation calls this prop an escape hatch that requires trusted and sanitized markup.

The safe default for a plain note is to keep the first component. The browser's textContent API provides the analogous text boundary for direct DOM code. When rich HTML is a genuine requirement, sanitize with a maintained allowlist sanitizer for the intended HTML policy after conversion and before the HTML sink, and check that no later library mutates the sanitized result. That ordering follows OWASP's sanitization guidance. A sanitizer is not a universal “safe string” label for URLs, CSS, JavaScript or future transformations.

Use a context map, not a single “escaped” flag

Final destination Cedar question Evidence to inspect
React text child or ordinary DOM text node Is note.body inserted as text? JSX children or textContent assignment and no later raw HTML rewrite.
Raw HTML sink Is user-authored or converted markup parsed? dangerouslySetInnerHTML, innerHTML, insertAdjacentHTML, sanitization policy and ordering.
URL attribute Does a note supply a link target? URL parsing, permitted scheme and destination policy, then attribute rendering.
Inline script or event handler Is a value embedded inside executable code? Exact script or handler context; generic HTML encoding is not enough.
Third-party renderer Does Markdown, syntax highlighting or a widget create HTML? Library version, configuration, output path and any post-processing.

The first two rows often settle the initial alert; the others stop a reviewer from declaring the whole feature safe. OWASP's JavaScript and TypeScript guidance specifically identifies framework bypasses and prefers text-building DOM APIs over raw HTML sinks. A URL provided to an href or src needs its own validation; escaping a quote or encoding a URL component does not decide whether the destination is acceptable. Modern framework versions may block some dangerous schemes, but the review must use the deployed version and application policy rather than a generic framework promise. Also inspect server-rendered bootstrap data: valid JSON is not automatically safe inside an HTML <script> element. Use the framework's documented serialization mechanism for that context; HTML parsing can occur before JavaScript interprets the JSON.

Find the transformation that happens after the sanitizer

Suppose the owner provides this fictional preview chain:

note.body
  → Markdown converter
  → HTML sanitizer
  → mention/link decorator
  → NotePreview raw-HTML sink

The sanitizer is present, but its result is not the final HTML. The decorator builds an anchor with a display name from a separate profile field. Even if the note was sanitized correctly, that later field has not passed through the same control. OWASP explicitly warns about modifying sanitized content afterward.

There are two defensible repair directions. Build the mention using DOM/text primitives with a separately validated URL, without reconstructing arbitrary HTML. Or perform all markup-producing transformations first and apply the intended sanitizer policy to the final result. The latter still requires checking that the renderer does not mutate it again. Simply adding a second call named escape is not a design argument.

Write the policy in terms of actual product features. For this example, the preview permits paragraphs, emphasis and links; it does not need images, inline style or embedded widgets. Link targets are parsed and limited to the application's approved scheme/destination policy. A string being valid HTML does not make its destination acceptable. If customer requirements include additional markup, review that requirement explicitly instead of expanding a generic “safe HTML” type silently.

Compare expected outputs without mistaking a marker for proof

Use the benign note Quarterly <b>update</b> to explain the two product contracts:

Viewer Intended result What a mismatch tells the reviewer
Plain activity row Literal angle-bracket text A later HTML rewrite may bypass the text component
Rich preview with permitted emphasis Formatting allowed by its conversion/sanitizer policy Conversion order or policy differs from the assumed chain
Admin moderation view Same documented policy or deliberately plain text A privileged viewer may use an unreviewed component
Email export Its own reviewed template/output policy Browser preview evidence does not cover this renderer

These are expected outcomes for future authorized validation, not reported observations. Rendering a bold tag proves only whether that marker is interpreted. It does not establish script execution or prove the sanitizer resists other inputs. Source inspection must still identify the actual policy, deployed dependency version and final sink.

In the completed synthetic review, NoteRow remains a rejected text-sink allegation. The preview has a supported control gap because the decorator introduces an independently editable field after sanitization; the precise browser consequence still needs validation. The moderation view remains unreviewed until its component is supplied. That is a more useful result than converting all three into one high-severity “stored XSS” ticket or declaring the feature safe because React escapes text.

Validate the fix without a live attack

For a plain note, a functional check in an authorized test environment can use inert sample markup such as a bold tag as data. It should appear literally in the text-only component; a rich-text preview should render only markup allowed by its documented sanitization policy. This checks a narrow rendering behavior without providing a script payload or claiming that every browser and route is safe. Also inspect the built component and any post-render library behavior at the new revision. If the preview is server-rendered elsewhere, verify that path separately.

If the fix replaces a raw HTML sink with text children, confirm the product still meets its intended content requirement. If rich text remains, check sanitizer version, policy, conversion order and all uses of the output. A merged patch, passing functional check, or absent scanner alert is not enough to close a previously verified issue when another viewer route is unreviewed. Record fixed, still present, or inconclusive with the remaining path.

Map the audience as well as the field

Cedar's activity note may be displayed to ordinary members, moderators, support staff, and email recipients. Those viewers can use different components and have different application authority. Record each actual destination and audience before assuming that a safe member-facing row settles the feature. The moderation view is particularly easy to omit because it may live in another frontend or load through an internal route.

The audience affects the possible consequence but does not prove it. A raw HTML sink in an administrator's view is a serious review lead, yet the reviewer still needs the input path, controls, and actual rendering context. Conversely, an ordinary user's view deserves the same factual trace even if its immediate business impact appears smaller.

For a stored note, record who can create and edit it, which viewers receive it, and whether a later import or synchronization can change it. A field described as “administrator-authored” may actually be populated from an external system. That provenance question determines the actor in the finding; it should not be answered from a database column name alone.

Make the rich-text policy inspectable

The synthetic preview permits paragraphs, emphasis, and links. Turn that product requirement into a documented output policy rather than a generic claim that a sanitizer is enabled. The developer should be able to identify the maintained sanitizer, relevant configuration, conversion order, and final sink for the deployed build. The source excerpt alone cannot qualify every version or plugin.

When a feature request adds images, mentions, or embeds, reopen the policy at that specific transformation. A new decorator can introduce markup after the original sanitizer, as the example already shows. A new link type can add destination behavior that ordinary text rendering never needed to consider. Review the feature's intended output and do not silently expand a shared “safe HTML” label across unrelated contexts.

The alternative plain-text repair can be simpler when formatting is unnecessary. Explain that product tradeoff directly: the activity row preserves the note's literal content while the separate preview implements a reviewed formatting policy. Developers then know why the two components use different sinks and which one a future feature should reuse.

Distinguish a safe marker from a security conclusion

A benign bold tag is useful for observing whether a string is treated as text or HTML. It does not prove which harmful inputs a sanitizer would reject, whether a URL policy is correct, or whether a privileged viewer shares the same component. Keep that observation narrow in the review record.

For the repaired decorator, the acceptance evidence should identify the exact final HTML construction and the independently editable profile field that previously bypassed sanitization. A functional check can confirm the intended display in an authorized environment, while source review establishes the ordering of transformations. If the built bundle or moderation component is missing, name that gap rather than extrapolating from the development preview.

This produces a precise closure decision: the specific post-sanitization construction was corrected, the text-only row remains a separate rejected allegation, and the unreviewed audience path still needs evidence. It preserves useful framework protections without granting them a wider scope than the code supports.

The next useful action is to annotate each note display with its source, final context and viewer. For server-rendered template expressions, continue with template injection review. For the wider method across interpreters, use injection review from input to operation.