Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool

JSON Injection Points and How to Debug Them

JSON injection usually is not a bug in JSON.parse() itself. The real problem is that untrusted input is allowed to change JSON structure while you build a payload, or that parsed JSON is handed to a query engine, inline script, or HTML sink without the right validation and escaping. If you are debugging broken API responses, mysterious extra keys, or payloads that only fail for certain user input, this is where to look first.

Fast diagnosis

  • Parsing fails only for certain names, comments, or query parameters: suspect hand-built JSON or broken escaping.
  • The JSON parses, but unexpected keys or operators appear: suspect client-controlled structure rather than a parser bug.
  • A page breaks around </script> or inline bootstrap data: suspect JSON embedded into an HTML <script> context without HTML-safe escaping.

What counts as a JSON injection point?

A JSON injection point is any place where user-controlled input can influence the shape of a JSON document or the way the application interprets that document. In practice, the highest-risk points are not the parser but the boundaries around it: manual serialization, inline HTML rendering, and server code that trusts a client-supplied object to define filters, permissions, or workflow rules.

It also helps to separate JSON injection from adjacent bugs. If untrusted JSON later flows into innerHTML, dangerouslySetInnerHTML, or another executable/HTML sink, the downstream issue is usually XSS. If a client controls a MongoDB-style filter object directly, the bug often becomes NoSQL injection. The handoff point is still the same: untrusted data was allowed to become structure.

Common JSON injection points in real apps

1. Manually building JSON strings on the server

This is the classic mistake. If you concatenate user input into a string that is supposed to become JSON, a quote, brace, comma, or bracket can change the payload shape or make it invalid.

Vulnerable example

const userId = req.query.userId;
const profile = getUserProfile(userId);

// DANGEROUS: user input is embedded into JSON text by hand
const responseBody =
  '{ "userId": "' +
  userId +
  '", "profile": ' +
  JSON.stringify(profile) +
  ', "status": "ok" }';

res.type("application/json").send(responseBody);

If userId contains 123", "role": "admin, the attacker has changed the structure of the JSON document rather than just the value of one field.

2. Embedding JSON into HTML or inline script state

This is a common modern debugging trap. JSON.stringify() creates valid JSON, but valid JSON is not automatically safe inside an HTML <script> tag. A value like </script><script>alert(1)</script> can terminate the script block unless you escape for the HTML context too.

Vulnerable bootstrap-state example

const state = { displayName: req.body.displayName };

const html =
  "<script>window.__BOOTSTRAP__ = " +
  JSON.stringify(state) +
  "</script>";

res.send(html);

The JSON is valid, but the browser parses the surrounding HTML first. This is why inline JSON often needs a framework-provided serializer or an extra HTML-safe escaping step.

3. Accepting raw JSON fragments for filters, rules, or queries

Some APIs let the client post a JSON blob that becomes a database filter, access-control rule, or search expression. The dangerous part is not JSON.parse(). The dangerous part is letting the client author the structure that your backend should control.

Risky query-shape example

const filter = JSON.parse(req.body.filter);

// Risk: the client now controls operators and field names
const users = await db.collection("users").find(filter).toArray();

When this happens, you are usually dealing with a validation and trust-boundary problem that can turn into logic abuse or NoSQL injection.

4. Legacy client-side parsing with executable APIs

Modern guidance is still straightforward: parse JSON with JSON.parse(), not eval() or new Function(). OWASP continues to recommend avoiding executable parsing paths because they turn a data problem into a code-execution problem.

Why these bugs are dangerous

  • Structure tampering: extra keys, changed array elements, or injected operators can alter authorization and business logic.
  • Broken parsing: malformed JSON can trigger exceptions, blank states, or failed background jobs.
  • Downstream injection: once the wrong object shape exists, later code may pass it into a database query, a template, or an HTML sink.
  • Hard-to-reproduce failures: these issues often appear only for specific input values, which is why precise logging matters.

How to debug JSON injection quickly

The fastest way to debug this class of bug is to treat it as a data-boundary problem. Capture the exact text before parsing or sending, compare the parsed shape to the schema you expected, and only then inspect the application logic that used the result.

  1. Capture the raw payload before parsing. Log the exact request body or exact outbound JSON text, not just the parsed object. If the problem involves encoding or invisible characters, raw text is what will expose it.
  2. Reproduce with the smallest payload that fails. Try inputs containing quotes, braces, brackets, commas, and the sequence </script>. If only those values trigger the issue, your bug is almost certainly at a serialization boundary.
  3. Check whether the JSON is invalid or merely dangerous. Invalid JSON produces parse errors. Dangerous JSON may parse cleanly but still contain extra fields, operators, or values that should never have been accepted.
  4. Search the code path for string assembly and unsafe sinks. Look for concatenation, template-built payloads, eval(), innerHTML, and direct use of client-supplied objects in queries or policy code.
  5. Validate the parsed shape immediately. Even when parsing succeeds, reject unknown keys, unexpected types, and unexpected nesting before the object reaches business logic.

Practical logging pattern

const rawBody = await request.text();
console.log("content-type:", request.headers.get("content-type"));
console.log("raw request body:", rawBody);

let parsed;
try {
  parsed = JSON.parse(rawBody);
  console.log("parsed keys:", Object.keys(parsed));
} catch (error) {
  console.error("JSON parse failed:", error);
  throw error;
}

// Validate immediately after parsing
assertUserSettingsSchema(parsed);

Common parser errors and what they usually mean

  • unexpected character: a quote, comma, brace, or other character ended up where JSON syntax does not allow it.
  • expected property name or '}': an object key is malformed, often because a value was concatenated into the document without proper escaping.
  • unexpected non-whitespace character after JSON data: one valid JSON value was parsed, then more attacker-controlled text followed it.

These errors are helpful, but a clean parse does not prove that the input is safe. If the payload parsed into the wrong object shape, you still have a real injection problem.

How to fix and prevent it

  • Build JSON from native objects or arrays and serialize once with the standard library. The same principle applies across ecosystems: JSON.stringify(), json_encode(), json.dumps(), json.Marshal(), and similar APIs.
  • When embedding JSON into HTML, use a framework helper or an HTML-safe serializer. Escaping for JSON syntax alone is not enough for the <script> context.
  • Validate schemas after parsing. Reject unknown keys and build database filters, sort clauses, and access rules on the server from allowlisted fields instead of trusting a client-supplied object.
  • Parse with JSON.parse(). Do not use eval() or other executable parsing shortcuts.
  • For browser-facing APIs, return application/json and use normal authentication and CSRF controls where relevant. Do not turn JSON endpoints into executable JavaScript responses.

Safer patterns

// 1. Build the response as data, then serialize once
const responseBody = JSON.stringify({
  userId: String(req.query.userId ?? ""),
  profile,
  status: "ok",
});

res.type("application/json").send(responseBody);

// 2. If you must embed JSON into an inline <script>, escape for HTML too
function serializeForInlineScript(value) {
  return JSON.stringify(value).replace(/[<>&]/g, (char) => {
    switch (char) {
      case "<":
        return "\u003c";
      case ">":
        return "\u003e";
      case "&":
        return "\u0026";
      default:
        return char;
    }
  });
}

Current note: do not confuse JSON injection with historical JSON hijacking issues in older browser behavior. They are related to JSON delivery, but they are different bugs. For modern apps, the most practical wins are still the same: serve real JSON with the correct content type, keep it out of executable contexts, and never let untrusted input author the structure you depend on.

Conclusion

When you debug JSON injection, focus on the boundary where text becomes structure. Capture the exact payload, identify whether the failure is invalid syntax or unexpected object shape, and trace where untrusted data was allowed to control JSON rather than remain a plain value.

In most cases, the fix is simple and durable: serialize once with standard JSON tools, validate immediately after parsing, and treat inline HTML, query objects, and other downstream consumers as separate security contexts with their own escaping and trust rules.

Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool