Skip to content
Data serialization Reviewed 2026-09-13

Safe JSON responses and HTML embedding

JSON encoding produces JSON syntax. It is not a general-purpose HTML, JavaScript-source, or attribute encoder. A correctly typed JSON API response containing a < character is not automatically XSS; the risk depends on how the response is served and how a consumer later uses its values.

Trust boundary: an API supplies data. A browser's HTML parser and a later DOM sink must not reinterpret that data as markup or executable code. Keep JSON parsing, HTML embedding, and DOM rendering separate.

JSON API responses

Use the framework's JSON response method and an appropriate content type, such as application/json. In an Express handler, after validating and authorizing the returned fields:

res.set('X-Content-Type-Options', 'nosniff').json({message});

res.json sets the JSON content type and serializes the object; it does not authorize the contents. The client should parse the response as JSON and render a message through a text sink. Feeding the parsed string into innerHTML can reintroduce XSS regardless of correct JSON syntax. See the Express response API.

Unsafe example: JSON inside an HTML script element

Even a non-executable script-data element is parsed by the HTML parser. An unescaped closing-script sequence in a JSON string can terminate the element.

const data = JSON.stringify({message});
const html = '<script type="application/json" id="bootstrap">' + data + '</script>';

Safer example: encode this specific HTML data context

This JavaScript helper accepts one bounded string. It serializes first, then encodes HTML-sensitive characters as JSON Unicode escapes, preserving the value when parsed as JSON.

function messageJsonForScript(message) {
  if (typeof message !== 'string' || message.length > 4000) {
    throw new TypeError('Invalid message');
  }
  return JSON.stringify({message}).replace(/[<>&\u2028\u2029]/g, character =>
    '\\u' + character.charCodeAt(0).toString(16).padStart(4, '0'));
}
const data = messageJsonForScript(message);
const html = '<script type="application/json" id="bootstrap">' + data + '</script>';

On the client, parse the element's textContent with JSON.parse; do not evaluate it. The escaped < prevents the HTML parser from recognizing a closing-script sequence inside the JSON. This helper is scoped to the shown script-data context: it is not an HTML attribute encoder or a sanitizer for later innerHTML use. If server templates escape the whole JSON string as HTML text, use the framework's documented embedding helper rather than adding an unreviewed raw-output bypass.

Rails JSON configuration and templates

For API responses, use render json: payload. Rails' JSON encoder can escape HTML-sensitive characters; changing escape_html_entities_in_json is a framework configuration choice, not a local variable that secures every serializer or consumer. Review the ActiveSupport JSON encoder.

When deliberately embedding JSON in a Rails HTML template, use the documented json_escape helper on serialized JSON and follow its context-specific rendering instructions. It converts characters into JSON escapes while preserving their decoded values. It does not sanitize arbitrary HTML or validate an object's authorization. See the Rails ERB::Util json_escape reference.

Regression test

Round-trip an ordinary message and a harmless fixture containing a closing-script sequence, ampersand, angle brackets, and Unicode separators. Assert JSON.parse(messageJsonForScript(value)).message === value, and assert the encoded data contains no literal <. Parse the constructed HTML with an HTML parser and confirm there is exactly one script-data element with no injected sibling elements. Test API content type and a consumer's text rendering separately. Do not execute a script payload as part of this regression test.

Related: HTML sinks, React HTML handling, and information disclosure.

Reference: CWE-79 — cross-site scripting.