HTML hygiene
Updated 2026-05-12What this is
Six patterns PreFlight scans for in .html, .jsx, .tsx, .vue, and .svelte files:
Inline event handlers:
<button onclick="doThing()">click</button>
Inline event handlers run inline JavaScript. Strict CSPs refuse to execute them. Code review skims over them because they look like attributes.
target="_blank" without rel="noopener":
<a href="https://external.example" target="_blank">link</a>
The opened page can call window.opener.location = '...' and navigate the original tab to a phishing page. Tabnabbing.
Mixed content:
<!-- on an https:// page -->
<script src="http://cdn.example/foo.js"></script>
Plain-HTTP resources on HTTPS pages. The script is modifiable by any network attacker between the user and the origin.
Inline <script> without CSP:
<script>
fetch('/api/me').then(...);
</script>
Inline scripts work, but they make CSP impossible to tighten. If there's no CSP, this looks fine; with CSP, inline scripts have to be allowed via unsafe-inline (defeats the point) or nonce-tagged (extra build complexity).
Forms posting over HTTP:
<form action="http://api.example/login" method="POST"></form>
Credentials submitted in cleartext over the wire. Any network observer reads them.
eval() / new Function() inside <script>:
<script>
const result = eval(userInput);
</script>
Same risk as Auth weaknesses covers, surfaced in HTML context.
Why it matters
Each pattern is a defense-in-depth bypass rather than a primary exploit. Inline handlers and inline scripts make CSP toothless. target="_blank" without noopener turns every external link into a tabnabbing vector. Mixed content destroys HTTPS guarantees for resources that arrive over plain HTTP.
In isolation, any one of these is a small risk. PreFlight flags them because they accumulate; a page with all six is operating without the browser-level controls modern web security depends on.
What the failure looks like
PreFlight scans HTML and JSX/TSX for the six patterns. JSX uses the same DOM attributes, so onClick="..." (string handler) on a JSX element produces the same finding as the HTML form.
What the fix looks like
Inline event handlers: move to a script with addEventListener.
<button id="thing-btn">click</button>
<script>
document.getElementById('thing-btn').addEventListener('click', doThing);
</script>
Or in JSX:
<button onClick={doThing}>click</button> // function reference, not string
target="_blank": add rel="noopener noreferrer".
<a href="https://external.example" target="_blank" rel="noopener noreferrer">link</a>
Mixed content: use https:// everywhere. If a third-party only ships HTTP, host the asset yourself.
Inline <script>: move to an external file with a CSP nonce or hash.
<script src="/static/init.js"></script>
Forms over HTTP: post over HTTPS. Forms touching credentials must always be HTTPS.
eval / new Function inside <script>: see Auth weaknesses for the broader fix.
Related
- Security headers covers the CSP that bounds inline-script and other risks.
- Auth weaknesses covers
evalanddangerouslySetInnerHTMLfrom the auth-side.
Sources
MDN's docs on target="_blank" security, mixed content, and CSP are the authoritative references. CWE-1022 names the tabnabbing class.
RELATED PROBES
- · HTML Hygiene
- · Security Headers