Reflected Payload Safety — when your API turns an error into an XSS delivery service

The fastest way to accidentally create a security problem is to be “helpful”. Especially in error messages. If your API mirrors user input back in the response (“you sent <script>...</script>”), you’re one bad UI decision away from shipping reflected XSS — even if your backend “only returns JSON”.

Rentgen has a simple rule: reject malicious-looking payloads and never echo them back. Not because developers are evil — but because developers are tired, and tired developers copy/paste.

What was tested

Rentgen injects a deliberately toxic payload into the request body:

{
      "body": "<script>alert(\"No XSS echo\")</script>"
    }

Then it checks two things:

  • Status code is honest: it should be 400 Bad Request or 422 Unprocessable Entity (anything else is suspicious for “you sent nonsense”).
  • No mirrored content: the response body must not include the exact payload (or a sanitized-but-still-executable variant of it).
Reflected Payload Safety check in Rentgen
Rentgen rejects script-like payloads and verifies the API doesn’t echo them back in error responses

What Rentgen found

  • TEST: Reflected Payload Safety
  • Expected: 400 or 422 + No Mirrored Content
  • Actual: 400 Bad Request + No Mirrored Content
  • Result: 🟢 Pass

Why this test matters (even if you “return JSON only”)

“But we don’t render HTML.” Great. Your API still feeds systems that do: web apps, mobile WebViews, admin dashboards, observability tools, support portals, log viewers, and that one internal debug page someone threw together on Friday.

Mirrored input becomes dangerous when any layer treats it as markup. That’s literally how reflected cross-site scripting works: attacker-controlled input is reflected back and executed in a victim’s context. :contentReference[oaicite:0]{index=0}

OWASP’s XSS guidance boils it down to a boring truth: untrusted data must be treated as untrusted — and you prevent execution through proper output encoding and safe handling. Echoing attacker input back is the opposite direction.

The real-world “how this bites you” scenarios

  • Frontend shows API error message as HTML. Someone uses innerHTML (or a templating shortcut) to display error.message from the API. Your mirrored payload becomes executable.
  • Admin/support dashboards render raw API responses. Support staff opens a “failed request details” page, and the payload executes in an authenticated back-office session.
  • Logs become an attack surface. Many log viewers render and linkify. Some even allow HTML/markdown rendering. If mirrored input flows into logs, you’ve created “stored XSS via logs”.
  • Security teams lose trust instantly. Even if it doesn’t pop XSS today, mirrored payloads in error responses are a bright red “you don’t control output” signal.

Why 400 or 422 specifically?

Because this isn’t an authorization failure. It isn’t a business rule issue. It’s malformed or unacceptable input. 400 says “your request is invalid”. 422 says “your structure is fine, your content isn’t”. Both are defensible. Anything else starts lying about what happened:

  • 200 — you accepted nonsense (or your validation is theatre)
  • 500 — you let user input crash you (now it’s reliability + security)
  • 401/403 — you turned bad input into auth noise

Why teams miss this

Because echoed input feels helpful during development. It’s the classic “we’ll improve error messages later” trap. And later never comes — it ships as-is, gets copied across endpoints, and suddenly you have a consistent, well-engineered vulnerability pattern.

Another reason: engineers assume JSON means “safe by default”. It’s not. JSON is just a container. The moment something renders it, it becomes a UI problem — and UIs are where XSS lives.

How to fix it (pragmatic, not academic)

The fix is surprisingly simple: never reflect raw user input in error responses. If you must reference the field, do it without including attacker-controlled content.

  • Prefer: {"error":"Invalid request","field":"body","reason":"script tags are not allowed"}
  • Avoid: {"error":"Invalid value: <script>...</script>"}
  • If you need debugging: log the raw input server-side with safe storage and access control, not in the client response.

When is echoing safe?

Almost never in the response body. If you’re absolutely sure the value will never be rendered (including by internal tools), you can argue it’s “fine”. In practice, that certainty is a fantasy. The safer approach: don’t echo it. Ever.

Why this check exists in Rentgen

Because I’ve seen it in the wild: an API rejects input, returns the exact payload in the error message, and then someone displays that message in a UI. Nobody planned an XSS exploit — it just happened through convenience.

Rentgen doesn’t care about your intent. It cares about the behavior: reject bad input, and don’t mirror it back. If it passes — great, move on. If it fails — fix it once, and you kill an entire category of “oops” incidents.

Final thoughts

This is one of those tests that feels “security-ish” until you realize it’s actually plain engineering hygiene. Your API’s error responses should be boring, consistent, and safe. Let the attacker shout into the void — not into your UI.