HTML Control Security
The HTML control uses a 6-layer XSS prevention pipeline. Each layer provides independent protection so that if one layer is bypassed, subsequent layers catch the attack. More than 28 known XSS vectors are blocked across these layers.
The 6-Layer Pipeline
Industry-standard HTML sanitiser. Parses the input into a DOM tree and strips disallowed nodes and attributes. Configured with ALLOWED_TAGS and ALLOWED_ATTR whitelists.
DOMPurify BEFORE_SANITIZE_ELEMENT and AFTER_SANITIZE_ATTRIBUTES hooks apply additional rules not covered by the standard whitelist. Strips javascript: in href values, data URIs in img src, and mXSS patterns.
HTTP header Content-Security-Policy set by the Atlas Forms server prevents execution of inline scripts even if a script tag somehow reaches the DOM. script-src 'self' blocks all inline script execution.
After DOMPurify runs, a secondary tag-list check validates every element in the sanitised output against the 17-tag allowed list. Any element not in the list is replaced with its text content.
All on* attribute names (onclick, onload, onerror, etc.) are stripped by a regex pass over the sanitised HTML string before it is inserted into the DOM. This is a final backstop for browser-specific DOMPurify bypasses.
All style attributes and <style> elements are removed. CSS injection can be used to exfiltrate data through background-image URLs or create phishing overlays. Blocking all inline styles is the safest approach.
DOMPurify Configuration
import DOMPurify from 'dompurify';
const ALLOWED_TAGS = [
'p', 'br', 'hr', 'div', 'span',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'strong', 'em', 'u', 'code', 'pre',
'ul', 'ol', 'li',
'a', 'blockquote', 'img'
];
const ALLOWED_ATTR = [
'href', 'target', 'rel', // a
'src', 'alt', 'width', 'height', // img
'class', 'id'
];
const config = {
ALLOWED_TAGS,
ALLOWED_ATTR,
ALLOW_DATA_ATTR: false,
ALLOW_UNKNOWN_PROTOCOLS: false,
FORCE_BODY: true,
};
export function sanitizeHtml(dirty: string): string {
return DOMPurify.sanitize(dirty, config);
}
Custom Hook: javascript: href Blocking
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
// Block javascript: protocol in href
if (node.hasAttribute('href')) {
const href = node.getAttribute('href') ?? '';
if (/^javascript:/i.test(href.trim())) {
node.removeAttribute('href');
}
}
// Block data: URIs in img src (potential XSS vector)
if (node.tagName === 'IMG' && node.hasAttribute('src')) {
const src = node.getAttribute('src') ?? '';
if (/^data:/i.test(src.trim())) {
node.removeAttribute('src');
}
}
// Force noopener on external links
if (node.tagName === 'A' && node.getAttribute('target') === '_blank') {
node.setAttribute('rel', 'noopener noreferrer');
}
});
Blocked Attack Vectors (28+)
| Category | Example Vector | Blocked By |
|---|---|---|
| Script injection | <script>alert(1)</script> | Layer 1 (DOMPurify tag whitelist) |
| Event handler | <img onload="alert(1)"> | Layer 1 + Layer 5 |
| JavaScript href | <a href="javascript:alert(1)"> | Layer 2 (custom hook) |
| SVG script | <svg><script>...</script></svg> | Layer 1 |
| Data URI | <img src="data:text/html,<script>..."> | Layer 2 (custom hook) |
| Style injection | <div style="background:url()"> | Layer 6 |
| CSS import | <style>@import url(evil.css)</style> | Layer 6 |
| mXSS | Malformed HTML parsed differently by browsers | Layer 1 + Layer 2 |
| Object/embed | <object data="evil.swf"> | Layer 1 |
| Form hijack | <form action="evil.com"> | Layer 1 |
| Meta redirect | <meta http-equiv="refresh"> | Layer 1 |
| Base tag | <base href="evil.com"> | Layer 1 |
| Inline script CSP bypass | <p></p><script>...</script> | Layer 1 + Layer 3 (CSP) |
Security Guarantees
- No JavaScript can execute from content rendered by the html control
- No external stylesheet can be loaded or injected
- All external links open in new tabs with
rel="noopener noreferrer" - No form elements can be injected to create phishing inputs
- No navigation redirects via meta tags